Core Web Vitals

Self-Hosting Google Fonts with size-adjust: Zero CLS Web Font Swap

By Joseph W. Anady · Published 2026-05-22 · Updated 2026-05-22

Web font swap is the silent CLS killer in 2026. The fallback paints in Georgia at 1.2x x-height, the web font swaps in at 1.05x, and every line of text shifts down by a few pixels. Lighthouse catches it. Google ranks lower for it. The fix is six lines of CSS most engineers never see.

Short answer: Self-host the font, declare a synthetic fallback face with size-adjust, ascent-override, and descent-override matched to the web font, and reference the synthetic fallback in your font cascade after the real font name. The fallback paints with corrected metrics, the swap is layout-neutral, and CLS goes to zero.

Why font-display: swap hurts CLS

The browser paints text immediately using whichever fallback the font cascade names. Almost every cascade lists Georgia, Arial, or system-ui as the fallback. Each has different x-height, cap height, and side bearings than the web font you actually want.

When the web font finishes loading, the browser swaps. Every line of text re-flows: maybe a single pixel of ascent, maybe 14 pixels per paragraph aggregated across the viewport. Lighthouse logs the shift as cumulative layout shift, the ranking signal that Google has been folding into search results since the Page Experience update.

A handledtax.com blog post we audited yesterday morning was running CLS 0.135 from a single web-font swap. Forty-five minutes after publishing the fix, the same page measured CLS 0.000.

The three-step fix

Step 1: Self-host the font

Pull the WOFF2 file directly from Google Fonts (or whatever foundry you use) and serve it from your own origin. Subset it if the latin range covers your content. Add a preload hint for the variant your above-the-fold copy actually uses.

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

Self-hosting also kills the third-party DNS lookup, the third-party TLS handshake, and the privacy compliance dance with Google Fonts. It is the right answer for almost every site that does not have an active retainer to maintain a foundry contract.

Step 2: Declare a synthetic fallback @font-face

This is the part most engineers miss. The @font-face rule does not have to point at a remote font file. It can point at a local font (Georgia, Arial, system) and override that font's intrinsic metrics to match the web font you are eventually swapping in.

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  ascent-override: 90.49%;
  descent-override: 22.56%;
  line-gap-override: 0%;
  size-adjust: 107.2%;
}
@font-face {
  font-family: 'Playfair Display Fallback';
  src: local('Georgia');
  ascent-override: 96.7%;
  descent-override: 21.4%;
  line-gap-override: 0%;
  size-adjust: 109.5%;
}

Those exact percentages are not invented; they come from the Next.js next/font/google generator, which has been tuning these values across the most common font pairings for years.

Step 3: Insert the synthetic fallback in the cascade

Update every CSS rule that names the web font to include the synthetic fallback between the web font name and the generic fallback. Cascades typically read like this before the fix:

--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-display: 'Playfair Display', Georgia, serif;

And like this after:

--font-body: 'Inter', 'Inter Fallback', system-ui, -apple-system, sans-serif;
--font-display: 'Playfair Display', 'Playfair Display Fallback', Georgia, serif;

The browser now resolves the cascade as follows: try Inter (web font, not yet loaded), try Inter Fallback (synthetic, paints with corrected metrics), then fall back to system fonts. When Inter finishes loading, the swap is layout-neutral because the synthetic fallback was sized to match.

Verification

Open PageSpeed Insights. Run the mobile audit. CLS should report 0.000 for any page whose layout shift was driven by font swap.

If CLS still nonzero after this fix, the culprit is something else: late-loading images without explicit width and height attributes, content that injects above existing content, or animation keyframes that touch layout properties.

What about font-display: optional?

An alternative pattern is to set font-display: optional, which tells the browser to render the fallback permanently if the web font does not arrive within 100 milliseconds. This eliminates CLS but at the cost of inconsistent typography between visitors. Some users see Inter, some see Arial. For most consumer brands the inconsistency is unacceptable. For an internal dashboard, it can be a clean tradeoff.

Wider implications

Core Web Vitals is one of the lightest-weight ranking signals Google has shipped, but for sites in competitive verticals it is the difference between top three and page two. Most agencies still treat performance as a final-step audit. Building for CLS, LCP, and INP from the first commit is faster than retrofitting.

Every ThatDevPro engagement ships with font swap CLS already at zero. It is not a paid add-on. It is what the studio considers baseline.