Fixing Cumulative Layout Shift Caused by Web Fonts Loading Late (FOIT and FOUT)
Text renders in a fallback system font the instant the page loads, then visibly jumps — reflowing surrounding content and shifting everything below it — the moment the real web font finishes downloading a second or two later. Core Web Vitals flags this as Cumulative Layout Shift, and it happens because two different fonts almost never occupy exactly the same amount of horizontal space for the same text.
The Problem
A page loads and text appears immediately in a system fallback font — Arial, Helvetica, or whatever the browser defaults to — which is good for perceived performance since there's no blank text area while waiting. A second or two later, once the custom web font finishes downloading, the text swaps to the real typeface, and everything on the page shifts: a heading grows taller or shorter, a button gets wider, and whatever content sits below reflows visibly. Chrome DevTools and PageSpeed Insights report this as a Cumulative Layout Shift (CLS) hit, directly affecting the page's Core Web Vitals score and, with it, its search ranking signal.
Why It Happens
Every font has its own metrics — different characters at different widths and different line heights for the same font-size
A fallback system font and a custom web font are, from the browser's layout engine's perspective, simply two completely different sets of glyph shapes and spacing rules. Even at an identical font-size, the same string of text will almost never measure out to the exact same width and line height in both fonts, because each typeface's internal metrics — character widths, x-height, line-gap — were designed independently of one another.
The browser has to make a choice about what to show before the web font arrives, and both default choices cause a visible shift
By default, most browsers either show invisible text until the web font loads (a "flash of invisible text," or FOIT) or show the fallback font immediately and swap once the real font arrives (a "flash of unstyled text," or FOUT). FOIT avoids a visible shift but introduces an invisible-text period that itself hurts perceived performance; FOUT avoids that delay but guarantees the layout shift this problem is actually about, since text is rendered, measured, and laid out once in the fallback font and then re-laid-out in the real one.
font-display defaults vary by browser and don't account for layout stability on their own
The CSS font-display property controls which of these strategies (and a few in-between variants) a given @font-face uses, but its default value — and even an explicitly chosen value like swap, which is commonly recommended for performance — only controls the timing of the text swap. Nothing about font-display alone makes the fallback font's box dimensions match the real font's, so the swap itself still causes layout shift even once FOIT's invisible-text penalty is solved.
The Fix
1. Set font-display: optional or swap deliberately, understanding what trade-off each one makes
@font-face {
font-family: "Brand Sans";
src: url("/fonts/brand-sans.woff2") format("woff2");
font-display: optional;
/* optional: uses the fallback immediately and only swaps to the
web font on a subsequent page load if it loaded fast enough —
never causes a shift after the page is already visible. */
}
font-display: optional treats the custom font as a nice-to-have rather than a requirement: if it isn't ready within a very short window, the browser commits to the fallback for that page view and never swaps mid-render, trading "sometimes shows the fallback font on a slow connection" for "never shows a layout shift" — the right trade-off whenever layout stability matters more than guaranteeing the brand font always appears.
2. When the web font must eventually appear (swap is required), size-match the fallback using font-size-adjust or a metrics-matched fallback stack
@font-face {
font-family: "Brand Sans Fallback";
src: local("Arial");
size-adjust: 104%;
ascent-override: 92%;
descent-override: 22%;
}
body {
font-family: "Brand Sans", "Brand Sans Fallback", Arial, sans-serif;
}
Rather than accepting whatever width and line-height a generic fallback happens to produce, defining an intermediate @font-face that maps a system font to adjusted metrics approximating the real web font's actual measurements lets the fallback occupy nearly identical space, so that when the swap to the real font eventually happens, there's little to no dimensional change left to shift.
3. Generate accurate size-adjust values using a metrics-matching tool rather than guessing
npx fontaine generate-fallbacks --font-family "Brand Sans" --font-path ./fonts/brand-sans.woff2 --fallback-fonts Arial,sans-serif
Hand-tuning size-adjust, ascent-override, and descent-override by trial and error is slow and error-prone; tools built specifically to compare two fonts' actual metrics (several widely-used build-tool plugins exist for exactly this) compute the precise adjustment values needed so the fallback's rendered dimensions match the target web font far more closely than an eyeballed guess.
4. Preload the critical web font so it has the best possible chance of arriving before the fallback is ever painted
<link
rel="preload"
href="/fonts/brand-sans.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Preloading the font file the page actually needs for its initial render starts that download as early as possible in the page's loading sequence, ahead of where the browser would otherwise discover the need for it from the parsed CSS — reducing how often a shift-prone swap scenario is even reached in the first place, on top of whatever metrics-matching or font-display strategy is also in place.
Why This Works
The layout shift isn't caused by loading a web font late — it's caused by the fallback and the real font disagreeing about how much space the same text needs. font-display: optional sidesteps the problem by refusing to shift after first paint at all; metrics-matched fallback fonts solve it at the root by making the "before" and "after" boxes nearly identical regardless of which font is swapped in; and preloading reduces how often the swap scenario is reached in the first place. Together they turn an unavoidable-seeming visual jump into something that either never happens or is too small to register as a Cumulative Layout Shift.
Conclusion
Layout shift from web font loading is a mismatch between two fonts' box dimensions for the same text, not an inherent cost of using a custom typeface. Choosing a deliberate font-display strategy, matching the fallback font's metrics to the real font's with size-adjust and the ascent/descent overrides, and preloading the critical font file together close the gap that Core Web Vitals is actually measuring.
