Most production sites ship 60-80% unused CSS — rules for components that don't appear on the current page, classes from libraries that were never used, old code nobody removed. Unused CSS slows page load, blocks rendering, wastes bandwidth on mobile. PurgeCSS and modern build tools can cut bundles by 80-95% in minutes. This guide walks through the measurement, the tooling, and the safelist patterns that prevent breakage.
PurgeCSS scans your HTML and JS for class names, then removes CSS rules whose selectors don't appear.
npm install --save-dev @fullhuman/postcss-purgecss
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.js', './src/**/*.jsx'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})
]
}
Use the Asset CleanUp plugin to unload CSS files page-by-page, or build a custom solution that scans wp_print_styles.
Tailwind v3+ does this automatically via the content config:
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{html,js,jsx,ts,tsx}',
'./public/**/*.html',
],
// ...
}
Tailwind only generates classes it sees in the content paths. If your Tailwind build is huge, content paths aren't covering all your templates.
.is-open, .has-error, .dark-mode get removed unless safelisted, breaking your site at runtime.// postcss.config.js with safelist
require('@fullhuman/postcss-purgecss')({
content: [...],
safelist: [
'is-open',
'has-error',
'dark-mode',
/^bg-(red|green|blue)-(100|200|300|400|500)$/, // colour utilities used dynamically
]
})
is-active, is-open, is-disabledhas-error, is-valid, is-toucheddark-mode, theme-blueis-loading, is-loadedEven after purging, sites still ship "all components combined" CSS. Better: each route loads only the CSS for the components it uses.
Next.js automatically code-splits CSS per route when you use CSS Modules or styled-jsx. Components imported by a route generate that route's CSS chunk.
Use dynamic imports for route-level code splitting:
// React Router lazy routes
const Product = lazy(() => import('./pages/Product'));
const About = lazy(() => import('./pages/About'));
Each lazy-loaded page gets its own JS chunk AND CSS chunk.
For fastest First Contentful Paint, inline the CSS needed for above-the-fold content directly in the HTML, defer the rest.
<head>
<style>
/* Critical CSS for above-the-fold: 10-20kb max */
body { font: 16px/1.5 sans-serif; margin: 0; }
.header { ... }
.hero { ... }
</style>
<link rel="preload" href="/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/main.css"></noscript>
</head>
Tools: Critical (npm package), Penthouse, or Next.js's built-in critical CSS extraction.
PurgeCSS is a build-time fix. Source-level cleanup matters too — old unused files take up developer mental space and grow over time.
# Find CSS files not imported anywhere
for f in $(find src -name "*.css"); do
basename=$(basename "$f" .css)
if ! grep -r "$basename" src/ --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" --include="*.html" --include="*.scss" -q; then
echo "Unreferenced: $f"
fi
done
After purging and splitting, re-run DevTools Coverage: