Note
What unused CSS actually costs a small site
An audit measures one page. The browser downloads for a site. On this one the two answers differ by a factor of 8 — and the bigger number was not the stylesheet at all.
- CSS
- Performance
- Static hosting
The number a per-page audit gives you
Point a coverage tool at a page on this site and it reports about a third of the stylesheet as unused. That is not the tool being wrong. Measured across every document that loads it, the median share of bytes inside rule blocks that match nothing on the requesting page is 32.7% — of 38,090 bytes of declarations, in 308 rules declaring 93 class selectors, loaded by 100 of the site's 105 HTML documents.
The spread is wider than the median suggests. The best page is
/ at 23.0%, because it has the
most markup on it. The worst is not a page anybody reads: one of
3 redirect stubs, /building-automation/, at
72.2% — 967 bytes of HTML that fetch
the whole stylesheet to print one sentence before leaving. Among the documents that
are not stubs the highest is /404.html at
47.9%.
So the number is real, and it is a statement about one page. The browser does not download for one page.
The same file, measured across every page
Ask instead which rules match nothing on any of the 100 documents and the answer is 1,502 bytes — 3.9%. Everything between that and the per-page figure is other pages' CSS: paid for once, served from cache afterwards, and used by the second page the visitor opens.
That is the number that decides whether to split a stylesheet per page, and on a site this size it decides against. The file is 19,129 bytes compressed. Splitting it would save a fraction of that on a first page view, lose the cache hit on every view after it, and buy a build that can ship a page missing a rule it needs.
Two different questions. “What does this page not use” is an audit. “What does this site not use” is a deletion list. Only the second is safe to act on, and on this stylesheet they differ by a factor of 8.
What was actually dead
The 1,502 bytes matching nothing anywhere are
12 rules, and they are not one category. Of them,
1,058 bytes in 9 rules are
reachable and have to stay. They are applied by a script, so the class never
appears in anything the build writes — and one of them is why a text search is
not good enough. The EDE checker sets a verdict chip
with "pl-chip pl-chip--" + kind, so pl-chip--bad and
pl-chip--warn exist nowhere on this site as whole words. They are
applied when a visitor's own file has errors in it. A remover that searched the
HTML, or even the HTML and the scripts, for whole class names would have deleted
the styling for the one state that matters.
Another 444 bytes in 3 rules match nothing and are kept on purpose: they are three of the four kinds of a theme the site does use, and a grouped rule further down the file names all four. Deleting them would remove a capability rather than waste. That is a decision, so it is written down as one, with its reason, where the gate can read it.
What was left was 1,046 bytes in 10 rule instances — a card component the home page stopped using, plus two orphans — checked against every HTML file, every script and the generator, and then deleted.
The rule that came out of it, and that now fails this build rather than living in somebody's memory: a rule matching nothing has to be reachable by a script, or named with a reason. Silence is what fails.
The mistake that hid eight rules
The first version of this measurement called a rule used if any class in its
selector appeared on a page. .pl-pillar .pl-chip passed, because
pl-chip is on nearly every page — and that selector cannot match
a document with no pillar in it, however many chips it has. A selector needs
every class in one of its comma-separated groups, and a rule is out of
reach only when every group has one missing. Fixing that found eight more rules and
moved every figure above.
The second mistake was the set of documents. The measurement walked every HTML file in the build, 5 of which load no stylesheet at all — the demo frames, an OG card and a brand preview. One came out as the worst page on the site, which is a figure about a stylesheet it never requests; worse, their class names were quietly protecting rules from being called dead. Both errors pointed the same way: an audit slightly wrong about its scope is wrong in the direction that hides waste.
The bigger number was in the head
While measuring the stylesheet it became obvious it is not the thing to measure. A first visit here is 104,992 bytes of font — 64,388 for the body face and 40,604 for the mono one — against 19,129 bytes of compressed CSS. The two faces are 5.5 times the stylesheet, and fonts arrive already compressed, so that is the whole figure. The whole-site measurement published here in October puts them at most of a page load, in its own transcript.
What a whole-site figure cannot see is which documents pay for what. The mono face
is drawn on 88 of the 100 documents that
load the stylesheet. Before this pass all 100 of them preloaded
it, so 12 fetched 40,604 bytes at high
priority and never drew a glyph with them. The demo frames were worse: they
preloaded both faces, 104,992 bytes, while loading no
stylesheet that declares either @font-face — so the browser could
not have used the bytes if it had wanted to.
Both are derived now rather than written into the template. Each document's preloads
come from the stylesheets that document actually loads: the faces from their
@font-face rules, and the markup that draws one from the rules reaching
its family, directly or through a custom property. A page earns a preload by having
markup that can match such a rule. 85 documents preload the
mono face today, and adding a mono rule to the stylesheet changes which ones on the
next build.
A preload is a promise that a file is needed before the layout has asked for it, and the browser keeps that promise at the expense of everything else on the page. Check the head before auditing the stylesheet.
What a byte count on the wire is worth
One figure here is not reproducible, and it is the one most performance budgets are written in. Five requests for the same stylesheet — same compression, same cache status, same age — came back as 19,784 / 19,798 / 19,851 / 19,798 / 19,875 bytes. A later burst the same day returned 19,804 five times. Nothing about the file changed; the edge compresses per machine, and a burst lands on one machine.
So the sizes published above are the ones anybody can reproduce: the file on disk at
66,753 bytes, and gzip -9 of it at
19,129. The variation is small. The part that matters is that a
budget pinned to a wire byte count is measuring somebody else's compressor on the
day it ran.
Doing this on your own site
Four things, in the order they pay:
- Measure the head before the stylesheet. Fonts, preloads and anything else with
rel="preload"on it are usually the larger number and always the earlier one. - Measure per page and across every page. The gap between the two is cache, not waste, and acting on the per-page figure is how a site ends up shipping a page without a rule it needs.
- Before deleting a rule, search the scripts — including for class names a script builds by concatenation, which appear nowhere whole.
- Sort what is left into three: reachable at runtime, kept for a named reason, and deletable. Then gate it, so the next rule that matches nothing is a build failure rather than a line nobody reads.
All of the above is this site measuring itself, which is the only site we are entitled to publish numbers about. The scripts behind it are the same kind of thing we build for a site that has to stay measurable and for a pipeline that gates its own claims.
Related
Where this comes up in the work
Website design
Remote website design services for small firms: a few real pages, fixed price, and a measured handover — HTTPS, broken links, Lighthouse, page weight.
Web, apps & DevOps
Web and app builds with the DevOps around them: front end, backend, the database under it, and the build, deploy and monitoring work that keeps it up.
More notes
Other things worth writing down
The one line that decides how a phone renders a page
With no viewport meta tag a phone lays a page out at desktop width, scales it down, and matches no media query. What the tag changes, and what not to add.
- Mobile
- Front end
- CSS
What a PX page actually weighs, and the switch that halves it
A PX view ships about 2.3 MB of framework JavaScript before your graphic. Gzip is off by default, and /module/* bypasses the filter that would do it.
- PX graphics
- JACE
- Performance
Why a Modbus network sits at 95% busy time
Busy time is the duty cycle of one poll thread per network, not host CPU. What it measures, where the time goes, and what actually brings it down.
- Modbus
- Performance
- Station engineering
Next step
Tell us the version, the hardware, and what it has to do.
You will get a written scope and a fixed price against it. If the honest answer is that you do not need us, you will get that instead.