The Web Font Loading Playbook

Font loading is one of the biggest performance blind spots on the web. Here's how to get it right without over-engineering it.

The Problem With Default Font Loading

When a browser encounters a custom font in your CSS, it does something counterintuitive: it hides the text until the font finishes downloading. This is called FOIT (Flash of Invisible Text), and it means your users stare at a blank page while font files travel over the network. On slow connections, this can last several seconds. On fast connections, it's still a perceptible flicker.

The irony is that the content is already there — the HTML has loaded, the layout is computed, the text exists in the DOM. The browser is just refusing to show it because the pretty font hasn't arrived yet. This is almost always the wrong tradeoff.

Step 1: Set font-display

The single most impactful fix is adding font-display to your @font-face declarations. This tells the browser what to do while the font is loading.

@font-face {
    font-family: 'YourFont';
    src: url('yourfont.woff2') format('woff2');
    font-display: swap;
}

swap tells the browser to immediately show text in a fallback font, then swap to the custom font once it loads. Users see content instantly, and the font pops in when ready. The swap is visible but brief, and it's a much better experience than invisible text.

optional is even more aggressive — the browser gives the font a very short window to load (about 100ms), and if it misses that window, the fallback font is used for the entire page view. The custom font is still downloaded in the background and cached for the next visit. This is ideal for repeat visitors and sites where layout stability matters more than brand fonts on first load.

Step 2: Preload Critical Fonts

By default, the browser doesn't start downloading a font until it encounters text that needs it — which means it waits for HTML parsing, CSS parsing, and layout before even requesting the file. You can skip this queue by preloading:

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

The crossorigin attribute is required even for same-origin fonts — this is a quirk of the fetch spec. Without it, the browser downloads the font twice.

Only preload 1–2 fonts. Preloading everything defeats the purpose by competing for bandwidth with other critical resources. Preload the font used for your body text (it's the largest text block and most visible during loading) and maybe your primary heading font. Anything else can load normally.

Step 3: Subset Ruthlessly

A full Noto Sans font with all Unicode ranges is over 1MB. The Latin subset is under 30KB. That's a 97% reduction. For most English-language sites, you need: basic Latin (A–Z, a–z, 0–9, common punctuation), Latin Extended-A (for accented characters like é, ñ, ü), and maybe a handful of symbols (arrows, bullets, currency signs).

If you're using Google Fonts, you get subsetting for free — their CSS API automatically serves subset files based on the Unicode ranges you're likely to need. If you're self-hosting, you need to subset manually before deployment.

Step 4: Match Your Fallback Font

The flash of unstyled text (FOUT) becomes jarring when your fallback font is wildly different from your custom font. If your body text is set in a slim humanist sans-serif but your fallback is Times New Roman, the layout shifts dramatically when the font swaps in.

The fix: use size-adjust, ascent-override, and descent-override in your fallback font definition to match its metrics to your custom font as closely as possible. This is called a "fallback font metric adjustment" and it dramatically reduces Cumulative Layout Shift (CLS).

@font-face {
    font-family: 'YourFont Fallback';
    src: local('Arial');
    size-adjust: 105%;
    ascent-override: 90%;
    descent-override: 22%;
    line-gap-override: 0%;
}

Getting these values right requires some experimentation. Tools like Fontaine and the Next.js font optimization feature calculate them automatically. The effort pays off in smoother page loads and better Core Web Vitals scores.

The Minimal Setup

If you take nothing else from this guide: use WOFF2, subset to Latin, add font-display: swap, and preload your body font. That combination handles 90% of font performance issues with minimal effort. Everything beyond that is optimization for sites where milliseconds matter.

More tutorials at Font-Converters.com →