Writing

The Performance Budget of a Typeface

·3 min read

The previous essay in this series argued that many applications should ship no fonts at all. This one is for the surfaces where the typeface earns its bytes — because "it's worth loading" is where the engineering starts, not where it ends. A webfont is a render-blocking dependency for the most important content on the page. Budget it like one.

One File, Every Weight

The single highest-leverage decision is the file architecture. A traditional family ships as static instances — regular, italic, bold, maybe a display cut — each its own request, easily 300–400KB together. A variable font carries the same range (often more: every weight from 100 to 900) in one file that typically weighs less than two statics.

A four-file static family at 380KB against one subset variable font at 92KB carrying the same range.

Fewer files also means fewer opportunities for partial loading — no flash of real-italic-fake-bold while the bold file straggles. And the variable file's extra axes are functional: optical sizing for small text, grade for dark themes — capabilities, not payload.

Subset Like You Mean It

Most font files are mostly characters your interface will never render. Subsetting — cutting the file to the scripts and characters you actually use — routinely halves a Latin font, and unicode-range lets you keep broader coverage without paying for it upfront: declare the Latin subset and the extended subset as separate faces, and the browser downloads the extension only when a matching character appears. Tools like pyftsubset/fonttools make this a build step, not a craft project. WOFF2 goes without saying; if you are shipping anything else in 2026, start there.

font-display, Chosen Honestly

The loading behavior is a product decision wearing a CSS costume:

  • block hides text while the font loads. Almost never defensible — you are trading your readers' time for your brand's vanity.
  • swap shows the fallback immediately and switches when the font arrives. Right for reading surfaces — but it makes the swap moment your problem, which is the next section.
  • optional uses the font only if it's already cached (roughly: instant or never). For high-performance applications that insist on a webfont, this is the honest setting — first visit reads in the fallback, every later visit gets the brand face, and nothing ever shifts.
Three font-display strategies on a timeline: block hides text, swap shows fallback then shifts, optional never shifts.

Engineering the Swap to Invisibility

If you choose swap, the layout shift is yours to eliminate. The modern toolkit does it almost perfectly: a fallback @font-face built on a local system font with size-adjust, ascent-override, and descent-override tuned so the fallback occupies the same space as the webfont. Text renders instantly in the adjusted fallback; the webfont arrives; nothing moves. This site does exactly this — the display face's Georgia fallback is metrically scaled so the swap is a change of voice, not a change of layout.

The same nominal size in two faces: x-heights disagree until font-size-adjust normalizes the fallback.

Finish with delivery hygiene: self-host (a third-party font CDN is a privacy question and a connection setup you don't need), preload the one critical file, and set immutable cache headers so the cost is paid once per user, ever.

The complete budget for a serious text face, done this way: one preloaded variable WOFF2, subset to your languages, ~40–90KB, swap with a metric-compatible fallback or optional outright. That is cheaper than most hero images — which is the standard the typeface had to meet all along.