Unused CSS is selectors and rules that the browser downloads, parses, and adds to the CSSOM but that never apply to a single element on the page. Performance audits flag a stylesheet when its unused bytes exceed 20 KB or 50% of the file. Because CSS is render-blocking, every byte of unused CSS directly delays First Contentful Paint and Largest Contentful Paint.
Think of unused CSS like installing a 200-volume encyclopedia just to look up one fact. The browser still has to shelve every volume (parse it into the CSSOM) before it's allowed to start reading — even though 70% of the volumes will never get opened.
CSS is render-blocking by design — the browser refuses to paint anything until it has the complete CSSOM, otherwise styles would flash on and off as new rules arrived. That means every byte of CSS, used or not, sits directly on the critical path to first paint.
Most unused CSS comes from a small number of structural causes, and the right fix depends on which one you have.
main.css linked from every page contains styles for the homepage, the checkout flow, the admin panel, and the blog — but each visit only renders one of those.Loading a full CSS framework on a page that uses ten components is usually 5–10× more CSS than necessary:
/* Bad: imports the full framework (~200 KB) */
@import 'bootstrap/dist/css/bootstrap.css';
/* Good: import only the partials you actually use */
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/mixins';
@import 'bootstrap/scss/grid';
@import 'bootstrap/scss/buttons';
@import 'bootstrap/scss/forms';
/* ...only the components your design uses */On a typical site this drops CSS from ~200 KB to ~30–50 KB without any visual change.
Like unused JavaScript, unused CSS shows up in synthetic audits (which scripts are bloated) and in field metrics (which pages actually feel it).
PurgeCSS scans your HTML, JSX, and template files, then strips every selector that doesn't appear. It plugs into PostCSS and works with every major bundler.
// postcss.config.js
const purgecss = require('@fullhuman/postcss-purgecss');
module.exports = {
plugins: [
purgecss({
content: [
'./src/**/*.{html,js,jsx,ts,tsx}',
'./public/index.html',
],
safelist: [
'active',
'show',
/^modal-/, // keep dynamically-added modal-* classes
],
}),
],
};Typical reduction on a framework-based site: 70–90%.
Tailwind's JIT compiler generates only the utility classes your markup actually uses. A site that imports the full Tailwind catalog (~3 MB uncompressed) ships ~10–30 KB after JIT.
CSS Modules, styled-components, vanilla-extract, and similar systems tie styles to the components that use them. When the component isn't imported, the CSS isn't shipped.
// Button.module.css ships only when Button is imported
import styles from './Button.module.css';
export default function Button({ children }) {
return <button className={styles.button}>{children}</button>;
}Frameworks like Next.js, Nuxt, SvelteKit, and Astro automatically split CSS per route — homepage visitors don't download the checkout's styles. If you're hand-rolling, configure your bundler to emit one CSS file per entry point.
Inline the styles needed for the above-the-fold content directly in <head>, then load the full stylesheet asynchronously. This unblocks first paint without making the rest of the page unstyled.
<head>
<!-- Inline critical CSS for above-the-fold content -->
<style>
body { margin: 0; font-family: system-ui; }
.hero { padding: 4rem 1rem; }
/* ...just the rules that affect the first viewport */
</style>
<!-- Load the rest without blocking render -->
<link rel="preload" href="/styles/full.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/full.css"></noscript>
</head>If you're using 5% of a CSS framework, write those 5% as plain CSS. Modern CSS handles flexbox, grid, custom properties, and container queries natively — most framework features are no longer needed.
Embeds and widgets ship their own CSS. Either inline only the styles you need, or load the widget's stylesheet asynchronously so it doesn't block first paint.
The fastest way to identify dead rules: open Chrome DevTools, run "Show Coverage" from the command menu, reload, and sort by unused bytes. The top three files in that list usually account for 80% of your total unused CSS.
What's happening: The site imports the entire framework bundle (~200 KB) but uses 10–20 components. 70%+ of the rules never apply.
Fix: Import the framework's individual partials instead of the full bundle, or run PurgeCSS in production. Typical saving: 100–150 KB.
What's happening: A single main.css contains styles for the entire site and loads on every page, even pages that need 5% of it.
Fix: Switch to a framework that splits CSS per route automatically (Next.js, Nuxt, SvelteKit, Astro), or configure your bundler to emit one CSS file per entry point.
What's happening: A class added by JavaScript at runtime (e.g. modal-open, is-active) doesn't appear in static templates, so PurgeCSS strips it. The class works in dev but breaks in production.
Fix: Add a safelist to the PurgeCSS config — either explicit class names or regex patterns like /^modal-/. Always test the production build of every interactive flow before shipping.
What's happening: The theme ships CSS for every layout, color scheme, and widget the user could enable, plus styles for editor variants used only inside the admin UI.
Fix: Most platforms expose a CSS minifier or tree-shaking option in their performance settings — turn it on. For deeper savings, run PurgeCSS against the rendered HTML at build or deploy time.
Synthetic audits use the same threshold model as unused JS: bytes wasted plus percentage wasted.
| Status | Unused bytes | Unused % | What it means |
|---|---|---|---|
| Good | < 20 KB | < 30% | Stylesheet is reasonably purged; further gains aren't worth the effort. |
| Needs improvement | 20–50 KB | 30–60% | Likely a CSS framework imported in full, or a site-wide stylesheet on every route. |
| Poor | > 50 KB | > 60% | Audits flag this; FCP and LCP are measurably delayed on mobile. Run PurgeCSS or split per route. |
Industry analysis (HTTP Archive Web Almanac) shows the median page sits firmly in the "Poor" band — most sites ship 60–70% unused CSS at p50, more than any other resource type.
Any selector or rule that the browser parses into the CSSOM but that doesn't match a single element on the rendered page. Synthetic audits measure this by walking the DOM and checking each rule against every element. A media query that never triggers, a class for a component you don't use, or a hover state for a button that isn't on this page all count as unused.
Audits flag a stylesheet when unused bytes exceed 20 KB or 50%. In practice the user pain becomes visible (a measurable FCP regression on mobile) once unused CSS exceeds ~50 KB.
Three structural reasons: most sites import a full CSS framework instead of just the partials they use, most sites link a single site-wide stylesheet on every route, and most CMS themes ship rules for every variant the user could enable. None of these are bugs — they're defaults that prioritize developer convenience over byte size.
It can, if you have classes that are added by JavaScript at runtime and don't appear in your static templates. Always configure a safelist for dynamically-added classes (regex patterns work well: /^modal-/, /^is-/) and test the production build of every interactive flow before shipping.
Yes — critical CSS is one of the highest-leverage FCP fixes available. Inlining the rules needed for above-the-fold content removes a render-blocking network round trip entirely. The trade-off is build complexity, which is why route-based CSS splitting (which most frameworks do automatically) is often a better default for sites that don't need the last 100 ms.
Indirectly. AI search systems most often surface pages that already rank well in traditional search — and Core Web Vitals are part of that ranking signal. A bloated stylesheet pushes LCP above the 2.5-second threshold, hurts your rankings, and reduces your odds of being chosen as a citation by Google AI Overviews or generative answer engines.
Run a Greadme deep scan — it surfaces every stylesheet with significant unused bytes and pairs each finding with an AI-generated fix. For a manual check, Chrome DevTools' Coverage tab shows used vs. unused bytes per file. Google Search Console's Core Web Vitals report tells you whether real users are feeling the FCP/LCP delay caused by render-blocking CSS.
Unused CSS is the largest waste category on the web — the median page ships 60–70% rules that never match anything. Because CSS is render-blocking, every byte sits directly on the critical path to first paint. A 50 KB purge is a 150 ms FCP improvement on mobile, with no visual change.
The fixes are mature and low-risk: PurgeCSS for traditional codebases, Tailwind's JIT mode for utility-first sites, CSS Modules or scoped styles for component-based apps, and route-based CSS splitting for everyone. Start with a Greadme deep scan to find the stylesheets that cross the 20 KB / 50% threshold, then fix them in order of bytes saved.