Tags: web-dev concept

Font Loading

Date: 2026-08-16


Fonts are discovered late, block text, and change layout when they arrive. The three problems are separate — invisible text, unstyled text, and shifted text — and each has its own fix.


What it is

Font loading is the sequence between the browser encountering a @font-face rule and text being painted in that font. It’s slow because fonts are referenced from CSS, so discovery happens after the stylesheet downloads and parses — two round trips in.

The three failure modes

FOIT   Flash of Invisible Text     text hidden until the font arrives
FOUT   Flash of Unstyled Text      fallback shows, then swaps
CLS    layout shift on swap        the swap changes text metrics, reflowing the page

You cannot have none of them. font-display chooses which you get.

font-displayBehaviourCost
autoBrowser decides — usually a short FOITUnpredictable
blockInvisible up to ~3s, then fallbackWorst for LCP if text is the LCP element
swapFallback immediately, swap whenever the font arrivesBest for LCP, worst for CLS
fallback~100ms invisible, short swap window, then fallback permanentlyReasonable compromise
optional~100ms invisible; if the font isn’t ready, never use it this visitZero CLS. Sometimes doesn’t apply your font

[CHECK: the block and swap period durations are implementation-defined and vary by engine — verify before quoting exact milliseconds.]

swap plus a metric-matched fallback is the usual right answer. optional is the correct choice when CLS is the binding constraint and brand typography is negotiable.

Killing the shift

The swap causes layout shift because the fallback and the web font have different metrics — different character widths and line heights, so text reflows. The fix is making the fallback occupy the same space:

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

Tuned properly, the swap becomes nearly invisible and CLS from fonts goes to zero. Tools exist to compute the overrides from a font pair, and several frameworks now generate them automatically.

Making them arrive sooner

  • Self-host. A third-party font origin adds DNS, TCP and TLS before the file even starts. Self-hosting removes a whole connection setup and is almost always faster now that cross-site caching is partitioned — see Browser Privacy Restrictions
  • Preload the fonts used above the fold, and only those:
    <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
    crossorigin is mandatory even when self-hosted — fonts are fetched in CORS mode, and omitting it causes a second download. This is the most common preload bug there is — Resource Hints
  • WOFF2 only. Every browser you care about supports it; shipping WOFF and TTF fallbacks doubles the payload for nobody
  • Subset. A Latin-only subset of a font is a fraction of the full multilingual file. unicode-range lets the browser download only the ranges a page actually uses
  • Variable fonts replace several weights with one file. Worth it when you use three or more weights; a single-weight site may be better off with a static cut

The budget

A realistic target for a retail site:

2 weights (regular, bold), Latin subset, WOFF2, variable
  ≈ 30–60 KB total, preloaded, self-hosted

Sites regularly ship 400KB of fonts across six weights and three families, most of which never render above the fold. The audit is quick: DevTools → Network, filter by font, and check how many actually load on a typical page.

Where it connects