Quick answer: Lighthouse flags CSS bytes that download but are never applied to the page. Reduce unused CSS by code-splitting stylesheets per route, removing legacy framework styles, using PurgeCSS or Tailwind JIT, and inlining only the critical above-the-fold CSS. Defer the rest with the media=print swap so the browser does not block on it.
Lighthouse fails this audit when your page downloads stylesheets that contain CSS selectors no element on the page actually matches. The browser still parses every rule, applies the matching ones, and discards the rest. Those bytes cost transfer time on slow networks and parse time on slow CPUs, and they delay first paint because CSS is render-blocking by default.
TL;DR
- What: Stylesheets contain CSS selectors that never apply to anything on the page.
- Why it matters: The browser blocks first paint while it downloads and parses every byte of CSS, including the unused parts.
- Fix: Split CSS per route, strip unused rules with PurgeCSS or Tailwind JIT, inline the critical above-the-fold CSS, defer the rest with the
media="print"swap pattern.
What does the "reduce unused CSS" audit check?
Lighthouse loads the page in a headless browser, runs the JavaScript, then walks every CSS rule and checks whether at least one element on the page matches its selector. Rules with zero matches are counted as unused. The audit reports the wasted bytes per stylesheet, sorted by potential savings.
The numbers are conservative. Lighthouse only flags rules that did not apply during the load it captured. Rules used by interaction-only states (:hover, :focus-visible, .is-modal-open) are sometimes counted as unused because the load did not trigger them. Treat the audit's "wasted bytes" as a floor, not a ceiling.
Why does unused CSS hurt performance?
Three different costs compound:
- Transfer. Every byte counts on a 3G or 4G connection. A 250 KB stylesheet at 50 KB/s adds 5 seconds before the browser can even start parsing.
- Parse and rule-matching. Once the file arrives, the browser parses every rule and adds it to the CSSOM. Then it walks the DOM and matches every element against every rule. Both passes scale with rule count, not just file size.
- Render-blocking. CSS in
<head>blocks first paint by default. The bigger your CSS, the longer the screen stays blank.
The Lighthouse FCP and LCP audits will keep failing as long as render-blocking CSS is on the critical path. Reducing unused CSS is one of the most direct wins for both.
How do I find what is actually unused?
Two reliable paths:
Chrome DevTools Coverage. Open DevTools, hit Cmd-Shift-P, type "Coverage", hit Show Coverage. Reload the page. Each stylesheet shows a green bar (used bytes) and a red bar (unused bytes). The percentage on the right is the share of unused CSS in that file.
Lighthouse JSON. Run a Lighthouse audit with --output=json. The unused-css-rules audit's details.items array lists every stylesheet with its wastedBytes, totalBytes, and wastedPercent. Useful if you want to script the analysis or compare runs over time.
Coverage is better for interactive exploration. Lighthouse JSON is better for automation and CI gates.
How do I reduce unused CSS?
Six moves, in roughly increasing order of effort:
1. Strip unused rules at build time
If you write your own CSS, use PurgeCSS:
npm install -D purgecss
npx purgecss --css styles.css --content '*.html' '*.js'
PurgeCSS scans your templates and scripts for class names and IDs, then removes any CSS rule that targets a selector it cannot find. Configure the content paths to scan every file that uses CSS classes (templates, components, JS that toggles classes dynamically). False positives are common when you build class names from variables (bg-${color}) · use a safelist for those.
If you use Tailwind, Tailwind JIT does the same thing automatically. Make sure your tailwind.config.js content array covers every file. A missing path means rules go missing in production · always check the production build before shipping.
2. Split CSS per route
Most modern frameworks ship per-route CSS automatically:
- Next.js / Astro / SvelteKit: route-level CSS extraction is on by default. Verify it in your network panel · each route should pull its own small CSS file, not one giant
app.css. - Vite: the
build.cssCodeSplitoption (default true) splits CSS per chunk. - Webpack: use
mini-css-extract-pluginwith one entry per route or per dynamic chunk.
If your build produces one giant CSS file for the entire app, every route pays the cost of every other route's styles. Splitting is the single biggest reduction you can make without touching any actual CSS.
3. Inline critical above-the-fold CSS
The CSS needed to render the first viewport should live inline in <head>:
<head>
<style>
/* critical above-the-fold styles, ~5-15 KB */
body { font-family: system-ui; margin: 0; }
header { ... }
h1 { ... }
</style>
</head>
The browser can paint as soon as the HTML and inlined CSS arrive · no extra round trip. Tools like critical (Node) and criticalCSS (built into Next.js and Astro) extract above-the-fold styles automatically.
4. Defer the non-critical CSS
For everything not needed for the first paint, load the stylesheet without blocking:
<link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
The browser treats a media="print" stylesheet as non-blocking (it does not apply to the screen). Once the load fires, swap media to all and the styles become active. The <noscript> fallback ensures the styles still apply if JavaScript is disabled.
Avoid the <link rel="preload" as="style" onload="this.rel='stylesheet'"> pattern. It triggers a synchronous style re-evaluation on swap, which Lighthouse now flags as a forced reflow. The media="print" trick has the same non-blocking effect without that side effect.
5. Remove legacy framework CSS
If you import a full CSS framework and use 5% of it, you ship 95% unused bytes:
- Bootstrap. Pulls in 200+ KB. If you use 4 components, copy those components into your own CSS and drop the framework.
- Old normalize.css or eric meyer reset. Replaced by
* { box-sizing: border-box; }andbody { margin: 0; }for most modern uses. - jQuery UI CSS. Almost always unused at this point.
Frameworks earn their weight when you use most of them or when you would rebuild a comparable design system from scratch. Otherwise they are pure dead weight.
6. Audit third-party stylesheets
Embedded widgets (chat, analytics, A/B testing) often inject their own stylesheets. You usually cannot strip these because you do not control the file. Two mitigations:
- Defer the widget load. If the chat bubble shows on user interaction, load its CSS and JS on interaction, not on page load.
- Block the stylesheet if you replace its styling. If you re-style the chat widget yourself, you may not need the vendor's CSS at all. Check with the vendor before stripping · some scripts assume their CSS is present.
How do I verify the fix?
Three checks, in order:
- Coverage report after the change. Open DevTools Coverage, reload, confirm the per-stylesheet unused percentage dropped.
- Lighthouse re-run. The
unused-css-rulesaudit should report fewer wasted bytes. Thefirst-contentful-paintandlargest-contentful-paintaudits should also improve, because CSS was on their critical path. - Real user metrics (RUM) if you have them. Lab improvements should translate to field improvements within a week. If they do not, you may have stripped a style that only applies on devices Lighthouse did not test.
Common reduce-unused-CSS mistakes
- Stripping dynamic class names.
bg-${color}produces classes PurgeCSS cannot find statically. Add them to the safelist. - Deferring critical CSS. If you defer the styles needed for the first paint, you get an unstyled flash. Always inline above-the-fold styles before deferring.
- Shipping the development CSS in production. Development builds skip purging for fast iteration. Verify your production build runs the strip step.
- Forgetting
:focus-visibleand other interaction states. PurgeCSS sees the load-state DOM, not the interaction-state DOM. Add interaction selectors to the safelist if your tooling flags them.
Related audits
- Reduce unused JavaScript, the JS twin of this audit. Same theme, different language.
- Eliminate render-blocking resources, CSS in
<head>is the primary render-blocker. The two audits move together. - Enable text compression, gzip or brotli cuts CSS transfer size 60-80% on top of whatever you strip.
- First Contentful Paint, the paint metric most affected by render-blocking CSS.
Audit your page now
Paste your URL, get scores plus a CLAUDE.md plan for Claude Code.