← All posts

Fonts are the slowest thing on your page

A typeface is a network request, a layout dependency and a rendering decision — in that order.

·3 months ago ·3 min read ·652 words

Open the network panel on almost any content site and sort by size. Somewhere near the top, usually above the images, you will find four or five font files. They were chosen in ten minutes from a dropdown and they are now the single most expensive decision on the page.

A web font is not one cost. It is three, and they arrive in sequence.

One: it is a network request that starts late

The browser does not know it needs a font until it has parsed the CSS, matched a rule to an element, and worked out which family that element resolves to. By then the stylesheet has already been fetched and parsed. The font request therefore begins deep into the load, on a connection that may be new, competing with everything else you asked for.

You can hurry this along with <link rel="preload">, and for a genuinely critical face it is worth it. But preloading four faces means four high-priority requests fighting each other, and the browser will honour your priorities right up to the point where it makes the page worse.

Two: it blocks or swaps, and both are visible

Until the file arrives the browser has to decide what to show. Historically it showed nothing — the flash of invisible text, a paragraph-shaped hole where your content should be. The modern default is kinder but not free:

@font-face {
  font-family: "Whatever";
  src: url(/f/whatever.woff2) format("woff2");
  font-display: swap;
  size-adjust: 96.4%;
  ascent-override: 90%;
}

font-display: swap renders the fallback immediately and replaces it when the real face lands. Nothing is invisible, which is the important win. The remaining problem is that the fallback and the real face almost never have the same metrics, so the swap reflows the text: line breaks move, the paragraph changes height, and whatever was under it jumps. If the reader had started reading, they lose their place. If they had reached for a button, they miss it.

This is what size-adjust, ascent-override and friends are for. You measure the fallback against the real face and scale the fallback until the two occupy nearly the same space. The swap still happens, but it stops moving the furniture. It is fiddly, it is worth doing, and it is a thing you now have to maintain.

Three: you probably shipped more of it than you use

A default download is often the full character set: Latin, Latin Extended, Cyrillic, Greek, a few hundred glyphs you will never render. Subsetting to the ranges you actually serve routinely removes half the file. Splitting by unicode-range lets the browser fetch only the block a given page needs. A variable font can replace four static weights with one file — but only if you use enough weights to break even, and one variable file is frequently larger than the two static faces most sites really need.

Then count the faces. Regular, italic, bold, bold italic is four files for one family. Add a display face for headings and you are at six before anyone has written a paragraph.

The option people skip

Use the fonts already on the device. A system stack costs zero bytes, zero requests, renders on the first paint with no swap and no reflow, and looks native on every platform because it is native.

--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--serif: "Iowan Old Style", Charter, Palatino, Georgia, serif;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

That is the stack this site uses. The prose you are reading is set in whatever serif your machine offers, and the trade is deliberate: the page is not pixel-identical everywhere, and in exchange the text is on screen in the first frame.

Custom type can be worth every byte. A brand often lives in its typeface, and I am not going to pretend otherwise. Just make it a decision with a number attached, rather than a dropdown someone touched during a redesign.

Related reading