CSS Optimization: critical CSS and purge unused styles
Unused CSS increases file size and slows FCP. Bootstrap with few components — 140 kB; after purge — 5–15 kB. Tailwind with full set — 3.5 MB; after purge — 5–50 kB.
Tailwind CSS: automatic purge
Tailwind 3+ removes unused classes at production build automatically based on content analysis:
// tailwind.config.ts
import type { Config } from 'tailwindcss';
export default {
content: [
'./resources/views/**/*.blade.php',
'./resources/js/**/*.{ts,tsx}',
// Important: include all files with classes
'./resources/js/**/*.json', // if classes generated dynamically
],
theme: { extend: {} },
plugins: [],
} satisfies Config;
Dynamic classes (string concatenation) — purge won't find them:
// Bad — purge won't see
const color = 'red';
<div className={`text-${color}-500`} /> // text-red-500 will be deleted
// Good — full class names
const colorMap = {
red: 'text-red-500',
blue: 'text-blue-500',
};
<div className={colorMap[color]} />
PurgeCSS for Bootstrap/custom CSS
// postcss.config.js
import purgecss from '@fullhuman/postcss-purgecss';
export default {
plugins: [
purgecss({
content: [
'./resources/views/**/*.blade.php',
'./resources/js/**/*.tsx',
'./public/**/*.html',
],
safelist: {
// Classes added by JS/Livewire/Alpine
standard: ['modal-open', 'show', 'active', 'fade'],
deep: [/^modal/, /^alert/, /^toast/],
greedy: [/swiper/],
},
defaultExtractor: content =>
content.match(/[\w-/:]+(?<!:)/g) || [],
}),
],
};
Critical CSS — inline styles for first screen
Idea: styles needed for above-the-fold rendering inline in <head>. Rest — load async.
Automatically via Vite (critters):
// vite.config.ts
import { defineConfig } from 'vite';
import { critters } from 'critters';
export default defineConfig({
plugins: [
critters({
preload: 'media', // async load for non-critical
pruneSource: true, // remove from external CSS what's already inline
logLevel: 'silent',
})
]
});
Manually for Laravel:
// Blade: inline critical CSS, async for rest
<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>
Critical CSS generation — critical npm package or penthouse:
npx critical https://example.ru/ --inline --minify > critical.css
CSS-in-JS — performance problems
Styled-components, Emotion generate CSS at runtime — additional JS load. For production prefer static CSS:
- Tailwind CSS — utility classes, no runtime overhead
- CSS Modules — local classes, compile to static CSS
- Vanilla Extract — typed CSS, zero-runtime
// CSS Modules in React
import styles from './ProductCard.module.css';
function ProductCard({ product }) {
return (
<div className={styles.card}>
<img className={styles.image} src={product.image} alt={product.name} />
<h3 className={styles.title}>{product.name}</h3>
</div>
);
}
Minification and compression
// vite.config.ts — LightningCSS for fast minification
export default defineConfig({
css: {
transformer: 'lightningcss',
lightningcss: {
targets: browserslistToTargets(browserslist('>= 0.5%')),
drafts: { customMedia: true },
}
},
build: {
cssMinify: 'lightningcss',
}
});
LightningCSS also automatically adds vendor prefixes and polyfills for configured browserslist.
Metrics
Goals after optimization:
| Metric | Before | Goal |
|---|---|---|
| CSS bundle (gzip) | 50–140 kB | < 20 kB |
| Tailwind after purge | 3.5 MB | 5–50 kB |
| Render-blocking CSS | Yes | No (critical inline) |
| CSS Coverage | 10–30% | > 80% |
DevTools → Coverage → reload page → see % unused CSS.
Optimization time: 4–8 hours for purge + critical CSS setup in build.







