Skip to content
Core Web Vitals specialists
Front-end optimisation

Finding and Removing Unused CSS for Faster Loads

How much CSS a page really needs, how to measure the unused share reliably and in which order to remove ballast without breaking the rendering.

13 min read CSSFrontendLadezeit

A stylesheet is rarely the largest item in a page load -- images and JavaScript weigh far more. Yet CSS decides when anything becomes visible at all: as long as a file linked in the document head has not been downloaded and parsed, the browser holds back the first paint. On the median mobile home page that means 77 KB (Web Almanac 2025) of CSS spread across 8 (Web Almanac 2025) requests. What makes that number interesting is not its size but its origin: those same eight files usually sit on every subpage, because they were assembled once for the entire site. The form rules of the contact page therefore hold up the blog article as well, the styles of a switched-off module keep travelling along, and nobody removes them because nobody can say with confidence where they are still needed. This article shows how to measure the unused share, what Critical CSS solves and what it does not, and in which order ballast can be removed without breaking the rendering. In our frontend optimization work, the stylesheet inventory is one of the first items we record.

Key takeaways

  • CSS is small, but it blocks: every file linked in the document head holds up the first paint until it has been downloaded and parsed. Only 15 percent (Web Almanac 2025) of mobile pages pass the Lighthouse audit on render-blocking resources.
  • The ballast does not come from bad rules, it comes from bundles: one set of stylesheets is built for the whole site and shipped on every subpage. At the 90th percentile that means 268 KB (Web Almanac 2025) of CSS on mobile and 31 (Web Almanac 2025) files per home page.
  • Measuring works with built-in tools: the coverage view of the developer tools counts used and unused bytes per file (Chrome for Developers), and Lighthouse lists stylesheets from 2 KiB (Chrome for Developers) of potential savings upwards. Both values apply to exactly the run the recording captured.
  • Critical CSS speeds up the visible area but does not clean up: the rest is still downloaded and parsed. In the example from the Chrome documentation, First Contentful Paint dropped by 0.2 seconds (web.dev), a 20 percent (web.dev) improvement -- while the ballast stayed in the bundle.
  • Removal becomes plannable once the structure allows it: cutting per template, conditional loading via media queries and cascade layers -- in stable browser versions since Chrome 99 (MDN) -- lower the risk that a deleted rule turns out to be missing somewhere else.

How eight stylesheets turn into invisible ballast

The path there is much the same in almost every project. It starts with a base framework and one file. Then a page builder for landing pages arrives that registers its styles on every page by default. Then an extension for reviews whose rules are loaded even where no review appears. Then a campaign with its own layout that gets switched off three months later -- the style file stays linked, because it hangs in a template nobody touches any more. What remains is a bundle covering the union of all pages and shipped on every single one of them. With themes and page builders the effect grows, because styles ship for modules that are not in use in this installation at all; the article on common slowdowns in WordPress installations puts the everyday consequences in order.

The distribution shows that this is not an edge case. At the 90th percentile a mobile page ships 268 KB (Web Almanac 2025) of CSS, and on home pages the number of CSS files rises to 31 (Web Almanac 2025). The pattern is old, too: back in the CSS analysis of 2022, the 90th percentile already carried 22 (Web Almanac 2022) stylesheets per page. So it is not the volume that is new, it is the expectation around load time. It adds up particularly clearly in shops with many templates -- home, category, detail page, cart, account -- as the article on loading times in Shopware projects shows along the typical levers.

Render-blocking does not mean 'slow to download'

A 20 KB style file arrives in a fraction of a second. It still blocks, because the browser holds back the first paint until every stylesheet linked in the document head has been parsed and merged into the CSS object model. With eight files, the page waits for the slowest of them and then for the parsing of all the others. That is why removing a small but late-responding file often helps more than shrinking the largest one. The field data matches: only 15 percent (Web Almanac 2025) of mobile and 13 percent (Web Almanac 2025) of desktop pages pass the Lighthouse audit on render-blocking resources.

Measuring the unused share reliably

Two built-in tools are enough to get started. Under its opportunities, Lighthouse lists every stylesheet whose unused share is at least 2 KiB (Chrome for Developers) and quantifies the potential savings in kilobytes. That is a rough sort: it tells you which file is worth a look, not which rule can go. How to order the findings of a report by effect instead of working through them from top to bottom is described in the article on reading and prioritizing a Lighthouse report.

The coverage view of the developer tools goes deeper. During a recording it tracks which bytes of a file were actually applied and contrasts the used and unused share per file (Chrome for Developers). The recording runs across a flow you define yourself: load the page, scroll, open the menu, submit a form. That is exactly where its value lies -- and its trap. It also pays to look at the server side, because a late-arriving stylesheet may simply be waiting on the response; how to break that time down is shown in the article on Server-Timing in the backend.

Where coverage measurement leads you astray

A coverage measurement is a snapshot of a single flow on a single page. Rules for states that did not occur during the recording -- an open menu, a form error, keyboard focus, print output -- show up as unused even though they are needed. Conversely, a rule counts as used as soon as it matches one single element, even if that element appears on one page out of two hundred. The picture only becomes reliable once you repeat the measurement across several templates and several states and merge the results: whatever was not touched in any recording is a candidate. Whatever appears in one single recording stays. Large documents make things worse, because more elements have to be checked against more selectors -- which is where the article on reducing DOM size fits in.

collect-rule-inventory.js
// Per stylesheet, count the rules that match no element on THIS page.
// Collect the result across several templates -- a single run proves little.
const report = [];

for (const sheet of document.styleSheets) {
  let rules;
  try {
    rules = sheet.cssRules;              // cross-origin sheets throw here
  } catch {
    report.push({ file: sheet.href, note: 'not readable' });
    continue;
  }

  let total = 0, noMatch = 0;
  for (const rule of rules) {
    if (!(rule instanceof CSSStyleRule)) continue;   // handle @media separately
    total++;
    try {
      if (!document.querySelector(rule.selectorText)) noMatch++;
    } catch {
      // selectors with ::before cannot be queried this way
    }
  }

  report.push({ file: sheet.href, total, noMatch });
}

console.table(report);

The report is a pointer, not a verdict. It counts rules, not bytes, it only sees what the document contains at the moment of execution, and it cannot judge rules for states that an interaction creates in the first place. Its value lies in repetition: run it across ten templates and lay the results on top of each other, and it quickly becomes clear which file contributes nothing worth mentioning on any template. If you want to collect such values continuously from the field rather than once in the browser, you need a transport path that does not slow the page down -- the article on fetchLater and deferred beacons shows how that works.

What Critical CSS solves -- and what it leaves open

The usual first step is Critical CSS: the rules for the immediately visible area move into the document as an inline block, and the rest is loaded in a way that no longer holds up the first paint. As a target size for that visible area, the Chrome documentation names 14 KB (web.dev) compressed, so the first paint gets by without further network round trips. The effect is measurable: in the documentation's example, First Contentful Paint dropped by 0.2 seconds (web.dev), a 20 percent (web.dev) improvement. What the cut looks like in practice and where it belongs in the build is described in the article on Critical CSS for the visible area.

The rest stays in the bundle

Deferred CSS is not saved CSS. The file is still transferred, parsed and attached to the object model, and every rule in it takes part in style calculation afterwards. The first paint gets faster; the work per page view stays the same.

The cut goes stale

Critical CSS applies to one particular viewport and one particular state of the template. Change the layout and the inline block no longer fits, and in the worse case a visible shift appears. That is why the cut belongs in the build process and not in a one-off piece of handwork.

Fonts remain a separate item

An inline critical block helps little if the font file holds the text back. Loading behaviour, preloading and fallback font all play a part here -- the article on web font performance optimization puts that in order.

MeasureFirst paintBytes transferredStyle calculation per page
Inline Critical CSSmuch earlierunchangedunchanged
Load the rest without blockingearlierunchangedunchanged
Cut per templateearliersmallershorter
Conditional loading via media queryearliersmallershorter
Delete rulesearliersmallershorter

Four ways to actually shrink the inventory

The first way is cutting per template. Instead of one bundle for the whole site, each page type gets its own, containing the shared base plus the rules of that template. The shared base stays cached, the template-specific part is small. The second way is conditional loading: stylesheets for print output, very wide viewports or a particular output medium get a media condition. The specification states explicitly that a browser may avoid fetching a conditional import as long as the import conditions do not match (W3C) -- so in the best case no blocking request is made at all. Both ways follow the same line of thinking as splitting JavaScript, which the article on lazy loading and code splitting describes for scripts.

The core in one sentence

Critical CSS takes the ballast out of the critical path, it does not remove it: as long as the same shared file ships on every template, every page pays for the rules of all the others -- the gain lies in cutting per template, not in the order of loading.

Deleting without breaking the rendering

The third way is what makes deleting bearable in the first place: a structure in which it is clear which rule overrides which other one. Cascade layers sort styles into named layers -- base framework, modules, exceptions -- and set the order once, instead of fighting it out through specificity and importance markers. They are available in the stable versions since Chrome 99 (MDN), Firefox 97 (MDN) and Safari 15.4 (MDN). Separate the layers cleanly and you can remove one of them without a rule from another layer suddenly winning. The fourth way is the least spectacular one: minification. 62 percent (Web Almanac 2025) of mobile home pages pass the corresponding Lighthouse audit -- the rest ship style files with indentation and comments. That replaces no cut, but it costs a single step in the build and works on every template. How cutting and inlining play together is shown once more by the article on Critical CSS for the visible area.

Three findings that are not findings

First, state rules: whatever applies only with an open menu, in an error case or on keyboard focus shows up as unused in a normal recording. Second, class names created at runtime -- if a script assembles classes from text fragments, no source analysis will find them again. Third, output variants: print view, high contrast and reduced motion only take effect under conditions a standard measurement does not create. So before deleting, add a comparison of the rendered pages per template and state -- before and after, with the same viewport.

An approach that works on a live site

On a running site, the grand gesture is rarely the right choice. Recut the whole bundle in one step and a rendering fault leaves you with no way to narrow down the cause. It is more sensible to follow an order that starts with the biggest lever and secures each step on its own. And so the gain does not evaporate two quarters later, a ceiling belongs at the end -- the same logic that the article on performance budgets for JavaScript describes for scripts can be transferred to the CSS volume per template.

  1. Record the inventory: which stylesheets are included on which template, how large are they, and which of them come from extensions that are still active?
  2. Measure coverage across at least five templates and the most important states, then merge the results -- only what was untouched in every recording is a candidate
  3. Remove whole files before individual rules: unregistering an extension that contributes nothing on any template carries less risk than trimming a file that is still needed
  4. Cut the remainder per template, keep the shared base separate, and bind print and special variants to a media condition
  5. Secure every step with a visual comparison per template and state, and add the CSS volume as a ceiling to your delivery, as described in the article on performance budgets in the build

The result is not measured by file weight but by the time until the first visible content. 1.8 seconds (web.dev) count as a good First Contentful Paint, measured at the 75th percentile (web.dev) of page loads and segmented across mobile and desktop. That is the more honest yardstick: a bundle that looks fine in the lab can still cost three seconds in the field on a slow connection. Where exactly the time goes and which template produces the worst values is what a technical performance analysis clarifies, with field and lab data side by side.

The question is rarely whether a rule is written elegantly. The question is whether the page downloading it needs it at all -- and that answer is in no file, it is in the measurement across all templates.

Project experience from performance work for shops and portals

Unused CSS is therefore less a housekeeping topic than a question of delivery: the rule is not the problem, the page that brings it along without cause is. Whoever knows the inventory per template, keeps the shared base deliberately small and measures every extension by what it contributes on which template gains twice: at the first paint and at every style calculation afterwards. How the effect of such work shows up in the metrics that search engines and users alike look at is set out on our page about the Core Web Vitals.

Sources and Studies

This article is based on data from the HTTP Archive Web Almanac (2022 and 2025 editions), on web.dev, on the developer documentation at Chrome for Developers, on the CSS specification of the W3C and on the browser compatibility data of MDN Web Docs. The figures quoted refer to the state of each publication.

Related Articles