Skip to content

3 min read760 wordsLast verified

Fonts and CLS — how web fonts hurt your layout score

Cumulative Layout Shift is the easiest Core Web Vital to fix once you know the cause. Most CLS problems trace back to two things: images without dimensions, and web fonts that swap after first paint. This guide covers the fonts half — what's happening, why it shifts, and the four font-loading patterns that prevent it.

What's actually happening

The browser loads your HTML and CSS before your custom font finishes downloading. While the font is in flight, it has to render text in something — either a fallback font from the system, or nothing at all. Three behaviors are possible:

  1. Render in fallback, then swap (font-display: swap) — text appears immediately, then jumps when the real font arrives. Big CLS hit.
  2. Render nothing for 100ms, then swap (font-display: block) — short blank period, then text appears. Small CLS hit but bad LCP.
  3. Render in fallback and don't swap if real font is late (font-display: optional) — only swaps on subsequent loads. No CLS hit ever.

The default browser behavior (without font-display) is auto, which is basically block — 3 second invisible-text period. That's why so many sites have FOIT (flash of invisible text) by accident.

Why swapping causes CLS

Fonts have different metrics — x-height, cap-height, character width, descenders. A 16px Arial is not the same physical size as a 16px Inter. When your fallback font is wider, every line of text takes more horizontal space, which can mean fewer characters per line, which means more lines, which means the content below shifts.

Even with the same line count, a font with different vertical metrics (taller ascenders, deeper descenders) shifts every paragraph below by 1–3 pixels per line. On a long page with 50 paragraphs, that's 50–150px of cumulative shift.

CLS counts every shift weighted by the area moved. A long article with one bad font swap can hit 0.15–0.30 CLS — well into "poor."

The four patterns that prevent it

1. Self-host with font-display: optional

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: optional;
}

With optional, if the font doesn't load in ~100ms, the browser renders in fallback and never swaps. CLS = 0. The downside: cached-vs-fresh-visitor inconsistency — first visit sees the fallback, second visit sees the custom font.

For SEO-driven sites, this is usually the right tradeoff. Google's crawl is "first visit" — and the fallback renders are perfectly readable.

2. Use size-adjust to match metrics

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

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

The fallback is configured to match Inter's metrics. When the swap happens, vertical positions don't change. No CLS even with font-display: swap.

Use Fontaine or Capsize to compute the right override values automatically.

3. Preload the critical font

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

Tells the browser to start the font request immediately, in parallel with HTML parse. By the time CSS is ready, the font is already in cache and renders without a swap.

Caveat: only preload one font per face. Preloading 6 weights wastes bandwidth and doesn't help. Pick the regular-weight + headings; let everything else swap.

4. next/font (Next.js apps)

import { Inter } from 'next/font/google';
const inter = Inter({
  subsets: ['latin'],
  display: 'optional',
  preload: true,
});

Next.js auto-self-hosts the font (no third-party request to fonts.gstatic.com), inlines critical CSS, computes size-adjust overrides, and preloads. One line of code, near-zero CLS impact. We use this on this site itself.

Fontsource does the same thing for non-Next.js projects.

The system-font escape hatch

If you have hard CLS problems and font flexibility doesn't matter to your brand, use the system font stack:

font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

Zero font downloads. Zero CLS from fonts. Page weight drops 50–150KB. The text uses each user's OS-native font — which most users prefer anyway.

This site uses system fonts only. Our CLS is effectively zero from fonts; only image-dimension or ad-slot issues can hurt it.

Quick diagnostic

Open DevTools → Network → filter "Font" → reload. Look at:

  • Number of font requests — should be 1–3 max. Over 5 is bloated.
  • Time to first byte — should be from your own origin or a fast CDN (Cloudflare/Vercel). fonts.googleapis.com adds 100–300ms.
  • Total transferred — should be under 100KB total. Way over that = too many weights.

Then DevTools → Performance → record 3 seconds → look for "Layout Shift" markers on the timeline. Hover each one. If the marker description mentions text re-layout, fonts are the culprit.

What our tool can show you

Search any domain on this site. If CLS is poor but the visible content seems fine, it's almost certainly fonts or images. Compare combined CLS to mobile CLS — fonts hit mobile harder because slower networks mean longer swap delays.

For CLS specifically, our fix-CLS guide covers images, ads, and animations alongside fonts. Fonts are usually the easiest of the four to fix.

By Paulo de Vries · Published

Check a site's vitals

Explore by industry

See how real-world sites in each vertical perform on Core Web Vitals.

Related guides

← All guides