Web Font Performance: Loading Fonts Without Slowing Your Site

Web fonts are usually the largest render-affecting resources on a page after images — and unlike images, they hold your text hostage. Done carelessly, they cause invisible text, layout jumps, and Core Web Vitals penalties. Done well, they cost almost nothing. This is the done-well checklist.

Understand the two failure modes

When a browser needs a font it doesn't have yet, it must decide what to do with the text meanwhile. The two bad outcomes have names: FOIT (flash of invisible text — the browser hides text while waiting) and FOUT (flash of unstyled text — it shows a fallback font, then swaps, causing a visible reflow). FOIT hurts users; FOUT hurts your Cumulative Layout Shift score and looks janky. Everything in this article is about minimising the wait and taming the swap.

The checklist

1. Serve WOFF2, and only WOFF2

WOFF2 uses Brotli compression and is roughly 30% smaller than WOFF; every browser that matters has supported it for years. If your @font-face blocks still list TTF, EOT, or SVG sources for ancient browsers, delete them — the fallback system font is a better experience than a 400KB TTF.

2. Load fewer fonts than you think you need

Each weight and style is a separate file (unless you use a variable font). A typical content site needs four at most: body regular, body bold, body italic, heading weight. Audit your CSS for weights you load but barely use — the 300 you tried once has a way of staying in the embed code forever. Fewer families helps more than anything else on this list; see why two families is usually enough.

3. Set font-display deliberately

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;  /* show fallback immediately, swap when ready */
}

swap is the sensible default for body text: content is readable instantly. For a decorative display face where the fallback looks jarring, optional is the honest choice — the font is used if it's already cached, skipped otherwise, and there is never a late swap. block (FOIT with a timeout) is almost never right.

4. Preload the critical font

Fonts are discovered late: the browser must download the CSS, parse it, and match it against the DOM before it even requests the font. For the one or two fonts used above the fold, skip the queue:

<link rel="preload" href="/fonts/inter-var.woff2"
      as="font" type="font/woff2" crossorigin>

Note the crossorigin attribute — font requests are CORS requests even from your own origin, and omitting it causes a silent double download. Preload only what's genuinely critical; preloading five fonts just moves the traffic jam.

5. Subset ruthlessly

A font file often carries Cyrillic, Greek, Vietnamese, and hundreds of glyphs your page never renders. Subsetting strips it to the character sets you use — typically cutting file size by half or more. Google Fonts does this automatically via unicode-range-partitioned files. For self-hosted fonts, tools like pyftsubset/fonttools or glyphhanger do the same job at build time. If your site is English-only, a latin subset of a text family should land in the tens of kilobytes.

6. Consider one variable font instead of four statics

A variable font replaces the whole weight range with one file and one request. For sites using three or more weights of a family, the variable version is usually the smaller total — and weights never pop in late. Check the file honestly, though: if you only use Regular and Bold, two subset statics may still win.

7. Self-host, or at least preconnect

Since browsers partitioned their caches (2020), the old "everyone shares the cached Google copy" benefit is gone — a font downloads once per site regardless. Self-hosting removes a DNS+TLS round trip to a third-party origin and gives you control over caching (set a long Cache-Control: max-age=31536000, immutable). If you stay on a CDN like Google Fonts, at minimum preconnect:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

8. Match your fallback to kill the layout shift

The remaining problem with swap is the reflow when the real font arrives with different metrics. Modern CSS can resize the fallback to match:

@font-face {
  font-family: "Inter-fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
}
body { font-family: "Inter", "Inter-fallback", sans-serif; }

Tools like Fontaine or the fallback-generator sites compute these override numbers for you. Result: the swap becomes nearly invisible and CLS from fonts drops to ~0. This is the single most underused technique on this list.

A sane loading recipe, summarised

  1. One or two families, subset to your languages, in WOFF2 — variable if you use 3+ weights.
  2. Self-hosted with immutable caching (or preconnected CDN).
  3. font-display: swap plus a metrics-matched fallback.
  4. Preload only the above-the-fold font, with crossorigin.

Follow that recipe and fonts stop appearing in your Lighthouse complaints entirely. And keep the system-font option in mind: for interfaces (as opposed to branded content), a system font stack delivers excellent typography for exactly zero bytes — the fastest font is the one you never download.