Skip to main content
All fixes

Page speed, redirects, Core Web Vitals

Fix render-blocking fonts: stop FOIT and font swap shifts

Web fonts loaded with default font-display block text rendering and cause layout shifts. Use font-display: swap, preload critical fonts, and self-host to fix FCP and CLS.

What's happening

By default, browsers wait up to 3 seconds for a web font before falling back (FOIT — Flash of Invisible Text). When the font finally arrives, the browser swaps fonts and the layout reflows because the font's metrics differ from the fallback (FOUT — Flash of Unstyled Text plus a CLS hit). Both behaviors hurt FCP and CLS simultaneously.

The font-display CSS descriptor controls this trade-off. font-display: swap renders fallback immediately, then swaps when the web font arrives — fast text but a CLS spike. font-display: optional renders fallback immediately and only uses the web font on the next navigation if it loaded fast enough — no CLS but you may not see the font at all on slow connections. font-display: block (the default in many setups) is the worst of both worlds for performance.

Lighthouse's "Ensure text remains visible during webfont load" audit flags fonts without font-display: swap or optional. The Network panel and Performance panel show font requests with their timing, and the LCP element analysis identifies when text-as-LCP is held up by font loading.

Why it matters

Render-blocking fonts delay FCP because text is the most common first-paint content. They also drive CLS up when the fallback and web font have different metrics — a 100% width container can shift several pixels vertically when the line-height changes mid-load.

On mobile 4G, font files (typically 30-150KB per weight) can take 500-1000ms to download. Without font-display: swap, that entire window is blank text. Users perceive this as a frozen page even when other content has already painted.

Common causes

  • Default font-display: auto/block in @font-face causes 3-second blocking period.
  • Web fonts loaded from third-party origins without preconnect or preload hints.
  • Multiple weights and styles loaded eagerly when only one is used above the fold.
  • Self-hosted fonts without proper Cache-Control headers.
  • Google Fonts CSS imported without &display=swap query parameter.
  • Variable fonts not used — separate files for each weight balloon total bytes.
  • Fallback font metrics not matched, causing CLS during font swap.

Detect this on your site

Run a quick scan with the Speed Test. The tool surfaces this exact issue with the records and context needed to apply the fix below.

Open Speed Test

How to fix it

  1. 1

    Add font-display: swap to every @font-face

    Open every @font-face declaration and add font-display: swap (or optional for higher-fidelity-or-nothing behavior). Google Fonts: append &display=swap to the CSS URL. Self-hosted: add the descriptor directly. Text becomes visible immediately.

  2. 2

    Self-host fonts and add preload

    Download the WOFF2 files from Google Fonts (or use next/font, which does this automatically) and serve them from your origin. Preload the critical weight:. The browser starts the font request alongside HTML parsing.

  3. 3

    Use next/font for zero-config optimization

    Next.js's next/font/google and next/font/local automatically self-host, subset, preload, and apply font-display: swap. It also generates a metric-matched fallback to eliminate CLS during swap. Drop-in replacement for any existing font setup.

  4. 4

    Match fallback metrics with size-adjust

    Use Capsize or fontpie to generate @font-face fallback metrics (size-adjust, ascent-override, descent-override, line-gap-override) that match your web font. The fallback occupies the same space as the web font, so swap doesn't shift the layout.

  5. 5

    Subset fonts to your actual character set

    Latin Extended subset of Inter is 30KB; the full multi-script file is 200KB+. Use Google Fonts' &subset= parameter or fonttools subset to ship only the characters your content uses. Ditch entire weights and italics that aren't used.

  6. 6

    Use a variable font for multiple weights

    One Inter variable font file (40KB WOFF2) replaces 9 weight files (270KB total). Variable fonts smoothly interpolate between weights and styles. font-weight: 400 to 800 just works.

  7. 7

    Audit unused weights and styles

    Open DevTools' Coverage tab, reload, scroll, and check which fonts are actually used. Italics, ultra-thin weights, and condensed styles are commonly loaded but never rendered. Drop them from the @font-face declarations.

Example

<head>
  <!-- Preload critical font weight -->
  <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
</head>

<style>
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Fallback metrics matched via Capsize */
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107.4%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body { font-family: "Inter", "Inter Fallback", sans-serif; }
</style>

Preload + variable font + metric-matched fallback for zero-CLS swap.

Frequently asked

Swap if the web font is part of your brand and you want it on every visit. Optional if you can live without the web font on slow connections in exchange for zero CLS. Most sites pick swap with metric-matched fallbacks.

Yes via next/font/local. Point it at your WOFF2 file and it does the same self-hosting preloading and fallback metric generation as next/font/google.

Use Capsize (capsizecss.com) or fontpie. Both generate the @font-face descriptor for any font you upload with metrics matched to common system fallbacks (Arial Helvetica system-ui).

Related fixes