All posts
PerformanceFontsCLS

How to Optimize Web Fonts for Performance

Web fonts can block rendering and cause layout shifts. Learn how to load fonts efficiently using preloading, font-display, variable fonts, and system font fallbacks.

debugutils.dev

Web fonts make sites look beautiful, but they come with a performance cost: they block rendering, cause layout shifts, and add network requests. Fonts are often responsible for 0.5–1.5 seconds of added load time if not handled carefully.

Here’s how to get great typography without the performance penalty.

Understanding the Problem

When a browser encounters a font referenced in CSS, it has to:

  1. Download the CSS file
  2. Parse the @font-face rule
  3. Start downloading the font file
  4. Decide what to show while waiting (invisible text, or fallback font)
  5. Swap to the real font when it loads

This creates two issues: delayed text rendering (FOIT/FOUT) and layout shifts when the real font swaps in with different metrics than the fallback.

Choose the Right font-display Strategy

The font-display descriptor controls what happens while a font loads:

@font-face {
  font-family: 'Inter';
  src: url('inter.woff2') format('woff2');
  font-display: swap; /* or optional, fallback, block, auto */
}
ValueBehavior
blockInvisible text until font loads (up to 3s) — bad for UX
swapShow fallback immediately, swap when font loads — common but causes CLS
fallbackBrief invisible period (100ms), then fallback, then swap — balanced
optionalBrief invisible period, only use custom font if loaded quickly — best for CLS

For Core Web Vitals: Use font-display: optional to eliminate CLS from font swaps. The custom font will still show on repeat visits (it’ll be cached). On first visit, users see the fallback font — design your fallback to match closely.

For most other sites: Use font-display: swap. Visible text is better than invisible text, even if there’s a brief flash.

Preload Critical Fonts

Add a preload hint in the <head> so the browser starts downloading fonts before it parses the CSS:

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

The crossorigin attribute is required even for same-origin fonts (due to how font loading works in the CORS spec). Without it, the preload is ignored.

Important: Only preload fonts that are definitely used on the page. Preloading unused fonts wastes bandwidth and hurts other resources.

Match Your Fallback Font Metrics

The main cause of CLS from fonts is metric differences — the fallback font takes up different space than the custom font, causing a layout reflow when the swap happens.

The size-adjust, ascent-override, and descent-override descriptors let you make a fallback font match the custom font’s space requirements:

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

body {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
}

When tuned correctly, the fallback and custom font take up identical space — making the swap invisible from a layout perspective.

Tools like fontpie.com can calculate the exact override values for any font pair.

Use Variable Fonts

Variable fonts contain multiple weights and styles in a single file — replacing what used to require 4–6 separate font files.

/* Instead of loading 6 font files */
@font-face { font-family: 'Inter'; src: url('inter-regular.woff2'); font-weight: 400; }
@font-face { font-family: 'Inter'; src: url('inter-medium.woff2'); font-weight: 500; }
/* ...etc */

/* Load one variable font file */
@font-face {
  font-family: 'Inter';
  src: url('inter-variable.woff2') format('woff2');
  font-weight: 100 900;
}

A variable font for Inter is ~90KB. Loading 5 individual weight files would be 200–250KB total.

Self-Host Your Fonts

Google Fonts requires a DNS connection and request to fonts.googleapis.com and fonts.gstatic.com. Even with preconnect hints, this adds latency.

Self-hosting:

  1. Download font files (use google-webfonts-helper)
  2. Host on your own CDN or server
  3. Write your own @font-face rules

Self-hosted fonts load from the same origin as your site — no extra DNS lookup, no external dependency.

Verify Font Performance

Run a test on debugutils.dev. The waterfall shows your font files as green bars — you can see their file sizes, when they start loading, and how long they take. If fonts are late-discovered (loading far into the waterfall), add preload hints.