Skip to content
HandbookPublic
Security

CSS, fonts and static assets

The render-blocking chain nobody audits: stylesheet size and splitting, self-hosted fonts with matched fallbacks, and cache headers that survive a deploy.

Written forFrontend engineers working on the critical rendering path
Reading time9 min read
Last reviewed2026-09-05

Stylesheets and fonts sit on the critical path by design: the browser will not paint text it cannot style, and it will not lay out text in a font it is still fetching. Everything in this article is about making that unavoidable blocking as short as possible, rather than pretending it can be removed.

CSS: ship the page's stylesheet, not the site's

A single global stylesheet means every visitor to your contact page downloads and parses the rules for your dashboard. Utility frameworks make this worse in one direction — one enormous file — and better in another, because the unused rules can be removed at build time if the content globs are right.

  • Check what the purge step actually kept. A class assembled at runtime (`text-${color}-500`) survives no static analysis; either safelist it or stop constructing class names dynamically.
  • Split by route where the framework supports it. Next.js does this per page automatically — the failure mode is importing a heavy component in a shared layout, which drags its CSS onto every route.
  • Inline the critical rules for above-the-fold content only if you have measured a gain. It removes one round trip and adds bytes to every document, including repeat visits that would have used the cache.
  • Delete the unused framework. We regularly find a UI library imported for two components, bringing its entire stylesheet with it.
bash
# Chrome DevTools -> Coverage -> reload
#
# 412 KB stylesheet, 11% used   -> the purge step is not working
#  38 KB stylesheet, 64% used   -> healthy for a component-based site
Coverage tells you what proportion of the shipped CSS the page actually used.

Fonts: self-host, subset, and match the fallback

A stylesheet link to a font host costs a DNS lookup, a TLS handshake and a second request before the font file is even discovered — on the critical path, on a mobile connection. Self-hosting removes all of it, and modern tooling makes it the easy option rather than the careful one.

01
Self-host in woff2 onlyEvery browser that matters supports woff2. Shipping woff and ttf alongside it doubles the storage and confuses the cache for no coverage gain.
02
Subset to the scripts you serveA full Latin-Extended plus Cyrillic plus Greek face is several times the size of the Latin subset you actually render. Where a site is bilingual, subset per script and load the second only on pages that need it.
03
Preload the faces used above the foldUsually two: one body weight, one display weight. Preloading six is a way of prioritising none of them.
04
Match the fallback metricssize-adjust and ascent-override on a local fallback make the swap invisible. This is the difference between a font that improves the design and one that costs you CLS.
05
Use font-display: swap, not blockInvisible text is worse than the wrong font. With matched metrics the swap is barely perceptible.

Cache headers that survive a deploy

AssetHeaderWhy
Hashed JS and CSSmax-age=31536000, immutableThe filename changes when the content does, so revalidation is pure waste
Fontsmax-age=31536000, immutableThey effectively never change; a new face is a new file
Unhashed images in /publicmax-age=86400, stale-while-revalidateReplaced in place occasionally, so allow a short window of staleness
HTML documentsno-cache, or a short s-maxage at the CDNMust revalidate, or a deploy does not reach anyone holding a cached document