Guide

Web font optimization explained: WOFF2, font-display, and Core Web Vitals

Custom typography is one of the fastest ways to make a site feel distinctive — and one of the easiest ways to wreck Core Web Vitals. A single 200 KB font file can delay Largest Contentful Paint (LCP) when it competes with your hero image, and a late font swap can shift headings enough to fail Cumulative Layout Shift (CLS). The fix is not "never use custom fonts." It is treating fonts like any other critical asset: pick efficient formats, load only what you need, declare how the browser should behave while files download, and match fallbacks so the swap is invisible. This guide walks through those decisions for static sites, SPAs, and content publishers who care about both design and HTTP caching.

Why fonts hurt performance

Browsers discover fonts late in the page load unless you tell them otherwise. A typical flow: HTML arrives, CSS is parsed, the stylesheet references @font-face rules or a Google Fonts URL, then the browser fetches font binaries. If your LCP element is a headline set in that custom face, LCP waits on the font download chain. Even when text renders with a system fallback first, swapping to the web font often changes glyph widths — lines reflow, buttons resize, and CLS spikes.

Two historical terms describe the visible delay: FOIT (flash of invisible text) when the browser hides text until the font loads, and FOUT (flash of unstyled text) when fallback text appears then swaps. Modern font-display values let you choose which trade-off you accept. The goal of optimization is to minimize both the wait and the layout jump.

What to measure

In Chrome DevTools Performance and Lighthouse, watch LCP element timing and layout shift entries tagged with font loads. In the field, CrUX reports aggregate LCP and CLS — font fixes that help lab scores but not real users usually mean caching or CDN issues. Pair font work with CDN edge caching so repeat visitors do not re-download unchanged files.

Choose the right format and weights

WOFF2 is the default choice in 2026. It uses Brotli compression and is supported by every modern browser. Ship WOFF2 only unless you must support very old clients — in that rare case add WOFF as a fallback inside @font-face, not TTF or OTF directly (those are larger and parse slower).

Load fewer files

Each weight and style you request is a separate download. A page that imports Inter 300, 400, 500, 600, 700, and 800 from Google Fonts may pull six files even if body text only uses 400 and headings use 600. Audit actual usage in your CSS and delete unused weights. For UI chrome, system UI stacks (system-ui, sans-serif) are often faster than another web font family.

Variable fonts

A variable font encodes a weight axis (and sometimes width or slant) in one file. Instead of separate 400 and 700 binaries, you load one file and set font-weight: 450 or animate weight smoothly. The file can be larger than a single static weight but smaller than three or four separate cuts — a net win when you need many weights. Check the font license; not every family offers a variable release.

font-display: swap, optional, and the rest

The font-display descriptor in @font-face (or the display= parameter on Google Fonts URLs) controls the first-render behavior:

  • swap — show fallback immediately, swap when the font arrives. Best default for body text and content sites; may cause a brief FOUT but avoids invisible text.
  • optional — use the web font only if it loads within a short window (~100 ms); otherwise stick with fallback for this page view. Excellent for decorative faces where system fonts are acceptable.
  • block — short invisible period then swap. Rarely appropriate; hurts LCP on slow networks.
  • fallback — invisible for ~100 ms, then fallback, then swap. A compromise few teams need once swap and size-adjust exist.

For publisher sites where readability matters, use font-display: swap on text faces and optional on display fonts used only above the fold accents. Pair swap with fallback matching (below) so the swap does not move layout.

Preload critical fonts — carefully

<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin> tells the browser to start downloading the font at high priority alongside CSS. Preload only the one or two files needed for above-the-fold LCP text — usually a regular and semibold cut, or one variable file. Over-preloading competes with hero images and JavaScript, hurting overall LCP.

Preload URLs must exactly match the @font-face src URL, including query strings. If you preload from a CDN hostname, the CSS must reference the same hostname or the browser downloads twice. Add long cache lifetimes on font files (see Cache-Control for static assets) because fonts change rarely.

preconnect vs preload

rel="preconnect" to fonts.gstatic.com only helps when you still load from Google Fonts. It opens TCP/TLS early but does not fetch the font. Self-hosting removes that third-party hop entirely — often the bigger win for privacy, predictability, and CSP simplicity.

Subsetting and self-hosting

A full Latin Inter font might be 90 KB WOFF2; a subset with only characters you use (Basic Latin plus a few symbols) can halve that. Tools like glyphhanger, fonttools/pyftsubset, or build plugins in Next.js and Astro generate subsets from your HTML/CSS content. For multilingual sites, ship separate subsets per script (Latin, Cyrillic, CJK) and load the right one per locale — do not send a 2 MB CJK font to English readers.

Self-host vs Google Fonts

Google Fonts is convenient: one CSS import, global CDN, automatic font-display. Downsides: extra DNS connection, privacy considerations, and CSS that may pull more weights than you intended. Self-hosting means you copy WOFF2 files into /fonts/, define explicit @font-face rules, and cache them on your origin or CDN. Build pipelines can download and subset fonts at compile time so production never depends on Google's CSS endpoint.

Either approach works if you subset, limit weights, and set font-display: swap. Self-hosting shines when you already fingerprint assets (inter-abc123.woff2) for immutable caching — the same pattern used for optimized images in web image pipelines.

Fallback font matching (size-adjust)

CLS from font swap happens because Arial and your custom sans-serif have different metrics — different x-height, cap height, and default advance widths. CSS size-adjust, ascent-override, descent-override, and line-gap-override on the fallback @font-face let you tune a system font until it occupies nearly the same box as the web font.

The workflow: load the web font in Fontaine, @next/font, or the fallback-font-metrics npm package to compute overrides, then emit a fallback face:

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

Use font-family: "Inter", "Inter Fallback", sans-serif; in your stack. When Inter loads, the swap is visually subtle and CLS stays near zero. This is one of the highest-leverage CLS fixes for content sites and pairs well with responsive layouts built in Flexbox and Grid.

Rendering strategies and SPAs

In client-rendered apps, fonts requested only after JavaScript hydrates arrive even later. If SEO or first paint matters, inline critical @font-face rules in the document head or server-render them — see SSR vs CSR trade-offs. Icon fonts (legacy Font Awesome webfont builds) are usually worse than inline SVG icons: they block text rendering, are bad for screen readers, and rarely subset well. Prefer SVG sprites or icon components.

Third-party embeds

Widgets, ad scripts, and chat bubbles often inject their own font CSS. You cannot control their weight, but you can defer non-critical widgets below the fold so they do not contend with your LCP font on the network waterfall. Reserve fixed height for ad slots to avoid CLS when those widgets load their typography.

Practical checklist

  1. WOFF2 only — drop legacy formats unless analytics prove you need them.
  2. Two weights max for most content sites, or one variable font file.
  3. font-display: swap on body/heading faces; optional on decorative accents.
  4. Preload only the LCP font file; verify no duplicate downloads in Network panel.
  5. Subset to characters and scripts you actually serve.
  6. Self-host when you want immutable caching and fewer third-party connections.
  7. size-adjust fallbacks to eliminate swap layout shift.
  8. Cache fonts for a year with fingerprinted filenames; purge on deploy when hashes change.
  9. Re-test LCP and CLS on slow 4G throttling after every font change.

Typography is part of performance engineering, not an exception to it. A fast site with a tuned font stack reads as more trustworthy — which matters whether you are shipping a product landing page or a long-form guide that monetizes through organic search.

Related reading