Skip to main content
SiteScan.top

Web fonts without the wait: font-display, subsets and preload

Invisible text, jumping layouts and an extra origin for every visitor — how to load custom fonts so that text appears immediately and stays put.

Performance · Published: 2 October 2026 · 7 min read · Марк Лебедев

A custom font is usually the largest piece of the design that blocks text. Until it arrives, the browser has to decide what to do with every heading and paragraph on the page: hide them or show them in something else. Both options have a cost, and the defaults rarely pick the cheaper one for you.

FOIT, FOUT and what font-display controls

FOIT — flash of invisible text — is the browser keeping text hidden while the font downloads. On a slow connection the page has a layout, images and buttons, and no words. FOUT — flash of unstyled text — is the opposite: text is drawn immediately in a fallback font and swapped when the web font arrives.

The font-display descriptor in @font-face chooses between them. It defines a short *block period* (text invisible) and a *swap period* (fallback visible, web font may still replace it):

swap trades visual stability for visible text; optional trades the brand font for stability. For most sites, swap on text fonts and optional on decorative ones is a reasonable starting point.

Ship fewer, smaller files

WOFF2 only. Every browser in current use supports it, and its Brotli-based compression beats WOFF and TTF clearly. Listing .woff, .ttf and .eot fallbacks in src adds nothing but maintenance.

Subset. A full font with Latin, Cyrillic, Greek and Vietnamese glyphs can be several times larger than what one page needs. Split it into subsets and let unicode-range tell the browser which file to fetch — it downloads a subset only if the page actually contains characters from that range:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-cyrillic.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

An English page then never downloads the Cyrillic file, and a Russian page gets both, since it still uses Latin digits and punctuation.

Limit weights and styles. Regular, bold and italic in two families is already six files. Ask whether the design really needs a 300 and a 600. Variable fonts help when it does: one file covers the whole weight axis (font-weight: 100 900). A variable file is usually larger than a single static weight, so it pays off from about three weights upward, not for one.

Self-host instead of a third-party font CDN

Loading from Google Fonts means a stylesheet from fonts.googleapis.com and then the font files from fonts.gstatic.com — two extra origins, each with its own DNS lookup, TCP and TLS handshake, discovered only after your HTML and CSS are parsed. The old argument that a visitor already has the font cached from another site no longer holds: all major browsers now partition the HTTP cache by top-level site, so a font cached on one site is downloaded again on yours.

Self-hosting puts the files on the origin the browser is already connected to, gives you control over subsets and cache headers, and removes a third-party request from every page view. The fonts' licence normally allows it; check before you copy.

Preload the one or two that matter

The browser learns about a font only after it has the CSS and finds text that uses it. A preload starts the download earlier:

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

The crossorigin attribute is required even for same-origin fonts: fonts are fetched in CORS mode, and a preload without it does not match the real request, so the file is downloaded twice. Preload only the font used above the fold — typically the main text face in one subset. Preloading every weight competes with CSS and the hero image for bandwidth and makes the page slower, not faster.

Make the fallback the same size

With swap, the moment the web font replaces the fallback, lines rewrap and blocks change height. That is layout shift, and it counts toward CLS. Font metric overrides let you shape a local font to match the web font's proportions:

@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; }

The numbers above are illustrative; calculate them for your font with a tool such as Capsize or Fontaine, or let your framework generate them. Done well, the swap becomes almost invisible.

Cache them for a year

Font files are ideal for long caching. Put a version or hash in the filename and serve:

Cache-Control: public, max-age=31536000, immutable

If fonts are on a separate CDN host, that host must also send Access-Control-Allow-Origin, or the browser will refuse to use them.

A SiteScan report checks one part of this: it flags web fonts declared without font-display and suggests setting font-display: swap.

Check-list

  1. Every @font-face has an explicit font-display.
  2. Fonts are served as WOFF2 only.
  3. Files are split into subsets with unicode-range.
  4. Only the weights the design uses are loaded; variable fonts where there are many.
  5. Fonts are self-hosted on your own origin.
  6. One or two critical fonts are preloaded with crossorigin.
  7. The fallback font has metric overrides to limit layout shift.
  8. Font files have versioned names and a one-year immutable cache.

Read next

Check how much of this is on your own site

A free scan surfaces broken links, JS errors and performance problems in about a minute.