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.
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.
# 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 siteFonts: 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.
Cache headers that survive a deploy
| Asset | Header | Why |
|---|---|---|
| Hashed JS and CSS | max-age=31536000, immutable | The filename changes when the content does, so revalidation is pure waste |
| Fonts | max-age=31536000, immutable | They effectively never change; a new face is a new file |
| Unhashed images in /public | max-age=86400, stale-while-revalidate | Replaced in place occasionally, so allow a short window of staleness |
| HTML documents | no-cache, or a short s-maxage at the CDN | Must revalidate, or a deploy does not reach anyone holding a cached document |