CSS Optimization: Critical CSS and Purge Unused Styles
Slow page load is often caused by excessive CSS. Render-blocking CSS delays the first contentful paint (FCP) and increases Largest Contentful Paint (LCP). Bootstrap out of the box weighs 140 KB, but after removing unused classes, it stays at 5–15 KB (a 96% reduction). Tailwind in development mode is 3.5 MB; after purge — 5–50 KB. We configure critical CSS and purge for any stack: from Laravel to Next.js, achieving FCP < 1 second. Our experience: 7+ years in performance optimization, over 50 projects. The result — passing Core Web Vitals without compromises. In 2–3 days, we implement critical CSS and purge turnkey.
How critical CSS affects Core Web Vitals metrics?
LCP (Largest Contentful Paint) — the rendering time of the largest element on the screen. If CSS loads synchronously (render-blocking), the browser doesn't paint content until full style load. Inline critical CSS eliminates this delay: styles for above-the-fold content are inserted directly into <head>, the rest loads asynchronously.
Example: on a Bootstrap site, only styles for the header, hero, and buttons end up in critical CSS — about 5 KB instead of 140 KB. LCP drops from 4 to 1.5 seconds (a 62% improvement). FCP decreases from 2.5 to 1.2 seconds.
What is purge CSS and how to configure it?
Purge removes classes not found in templates. In Tailwind 3+, purge is built-in via the content config:
// tailwind.config.ts import type { Config } from 'tailwindcss'; export default { content: [ './resources/views/**/*.blade.php', './resources/js/**/*.{ts,tsx}', './resources/js/**/*.json', // if classes are generated dynamically ], theme: { extend: {} }, plugins: [], } satisfies Config; Dynamic classes (string concatenation) — purge won't find them:
// Bad — purge will not see const color = 'red'; <div className={`text-${color}-500`} /> // Good — full class names const colorMap = { red: 'text-red-500', blue: 'text-blue-500', }; <div className={colorMap[color]} /> What is safelist and why is it needed?
Safelist is a whitelist of classes that purge should not remove. Sometimes classes are added via JavaScript, for example, Bootstrap modals: modal-open, show. If they are not found in HTML templates, purge will remove them — breaking functionality. We configure safelist in PurgeCSS config:
// postcss.config.js import purgecss from '@fullhuman/postcss-purgecss'; export default { plugins: [ purgecss({ content: [ './resources/views/**/*.blade.php', './resources/js/**/*.tsx', './public/**/*.html', ], safelist: { standard: ['modal-open', 'show', 'active', 'fade'], deep: [/^modal/, /^alert/, /^toast/], greedy: [/swiper/], }, defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [], }), ], }; Step-by-step purge CSS configuration
- Audit current CSS. Use Coverage in DevTools, bundle analysis, identify render-blocking.
- Configure purge. Set up PurgeCSS or Tailwind JIT, specify paths to templates.
- Create safelist. Add classes used dynamically (modals, alerts, animations).
- Testing. Check that nothing broke, visual regression.
- Deploy. Integrate purge into the build (Vite, Webpack, Laravel Mix) and deploy to production.
Choosing a strategy: runtime CSS vs static CSS
CSS-in-JS (styled-components, Emotion) generates styles at runtime — additional JS load. For production, static CSS is preferable.
| Parameter | CSS-in-JS | Static CSS (Tailwind, CSS Modules) |
|---|---|---|
| Runtime overhead | Yes | No |
| Bundle size | +50–150 kB (runtime) | 0 kB |
| Tree-shaking | Complex | Simple |
| SSR support | Requires setup | Out of the box |
Static CSS is better for Core Web Vitals: it doesn't block rendering (after critical inline) and loads CPU less.
For Vite, critical CSS is configured via critters:
// vite.config.ts import { defineConfig } from 'vite'; import { critters } from 'critters'; export default defineConfig({ plugins: [ critters({ preload: 'media', pruneSource: true, logLevel: 'silent', }) ] }); In Laravel, critical CSS is inserted manually:
<style>{!! file_get_contents(public_path('css/critical.css')) !!}</style> <link rel="preload" href="{{ mix('css/app.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript> <link rel="stylesheet" href="{{ mix('css/app.css') }}"> </noscript> Minification — done by LightningCSS in Vite or cssnano. Output: CSS bundle < 20 KB, coverage > 80%.
Metrics comparison before and after optimization
| Metric | Before optimization | After |
|---|---|---|
| CSS size (Bootstrap) | 140 KB | 5 KB |
| LCP | 4.0 s | 1.5 s |
| FCP | 2.5 s | 1.2 s |
| TTFB | 0.8 s | 0.6 s (unchanged) |
Common mistakes in CSS optimization
- Ignoring dynamic classes — purge removes them, breaking the interface.
- Incorrect safelist — modals, alerts stop being styled.
- Critical CSS not updated when content changes.
- Forgetting about iframes and third-party scripts — their CSS also affects metrics.
- Over-optimization: removing classes used in non-obvious places (archives, static pages).
What's included in CSS optimization work?
- Audit of current CSS: Coverage in DevTools, bundle analysis, identification of render-blocking.
- Purge configuration: PurgeCSS or Tailwind JIT config, safelist creation.
- Critical CSS extraction: automatically via critters or manually using
npx critical. - Minification and compression: LightningCSS / esbuild, Gzip / Brotli.
- Integration into the build: Vite, Webpack, Laravel Mix, Next.js.
- Metrics check: LCP, FCP, TBT before and after.
- Documentation: description of settings, recommendations for maintenance.
Result: passing Core Web Vitals, minimal CSS bundle size, preserved functionality and styles.
We use PurgeCSS, Tailwind JIT, critters, and PostHTML depending on the stack. Everything is configured for your build.
Contact us for a detailed audit of your project. Get a consultation on suitable tools. We will assess your CSS in 1 day. Order CSS optimization and speed up your site.







