CLS Failures — Font Swap Shifts the Layout
Fix layout shifts caused by late web font swaps and FOUT, reserve space with size-adjust and font metrics overrides, and preload critical fonts with display swap.
Symptoms
- Text renders in a fallback font then visibly jumps when the web font arrives (FOUT)
- Lighthouse CLS exceeds 0.05 with layout shift entries attributed to text elements
- Headlines reflow lines and push content down seconds after first paint
Possible causes
- The web font loads after first paint with font-display: swap, and its metrics differ from the fallback
- No preload on the font file, so it waits behind CSS and other resources in the critical path
- The fallback font metrics differ enough (line height, advance width) to reflow every paragraph
- Multiple font weights load progressively, each swap shifting the affected text
Diagnosis steps
Identify the shifting elements
DevTools → Performance → record load → Layout Shift regions; or the Layout Shift Instability entriesInterpretation
Shift entries name the elements and the impact score; text nodes implicating fonts confirm the mechanism.
Check font loading order
DevTools → Network → Font filter: when does the woff2 arrive relative to first paintInterpretation
Fonts arriving after paint with a fallback visible first is the swap; the duration of both states determines perceived shift.
Compare font metrics
npx font-stats "Helvetica Neue" "Inter" 2>/dev/null || layout inspection of both statesInterpretation
Advance-width and line-height differences between fallback and target quantify the reflow each swap causes.
Root cause
With font-display: swap, the browser paints fallback text immediately, then replaces it when the web font arrives. If the two faces have different metrics — different advance widths and line heights — every line rewraps and every block resizes, shifting content below and accumulating CLS.
Fix
Preload the critical font files, subset them to cut size, define a fallback font with matched metrics using size-adjust, ascent-override, descent-override, and line-gap-override, and keep font-display: swap so text stays visible throughout.
Risk note
Blocking text render until fonts arrive (font-display: block for long periods) converts CLS into invisible text and worse LCP. Match metrics instead.
Verify the fix
Lighthouse CLS returns under 0.05, layout shift entries for text disappear, and the visual font swap becomes imperceptible in a throttled-network replay.
Symptom walkthrough
The shift has a narrative users can see: headline appears in the system font, then jumps one line taller and pushes everything down when the web font lands. The DevTools record confirms it mechanically:
LayoutShift: score 0.12
sources: <h1 class="hero"> ... impact region covering the fold
And the Network panel orders the story: woff2 finishing 1.8s after first paint, in a swap configuration. The metric difference is the physics — if the fallback is 5% narrower, every wrapped line changes, and every block below inherits the displacement.
Fix in practice
Preload the critical faces so swap happens early:
<link rel="preload" href="/fonts/inter-var.woff2"
as="font" type="font/woff2" crossorigin>
Match the fallback metrics to the target:
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 100.6%; /* tuned to Inter advance width */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body { font-family: Inter, Fallback, sans-serif; }
Keep swap so text is always visible:
@font-face {
font-family: Inter;
src: url(/fonts/inter-var.woff2) format('woff2-variations');
font-display: swap;
}
And subset the file: unicode-range covering Latin-1 plus the exact glyphs in the viewport headline cuts bytes and moves the swap before first paint entirely.