Performance
font-display and its CLS cost
font-display decides what the browser shows while the webfont downloads: swap costs CLS, block hides the text. The five values and how to cut the gap.
Key points
- swap shows the fallback at once and moves the layout when the webfont arrives.
- block keeps the text invisible for about 3 s: low CLS, worse reading.
- size-adjust and the override metrics cut the gap between fallback and webfont.
font-display decides what the browser shows while the webfont downloads: swap shows the fallback immediately, optional gives the font up on slow networks, block hides the text. None of the five values is free in CLS.
The five values, in a table
| Value | Text block | What the reader sees | Cost |
|---|---|---|---|
| auto | The browser decides | Depends on the browser | Unpredictable |
| block | About 3 s | No text | Invisible text, then swap |
| swap | 0 ms | The fallback | A metric gap |
| fallback | About 100 ms | The fallback, then the webfont | Short gap, then stable |
| optional | About 100 ms | The fallback | None on slow networks |
The milliseconds are the ones documented on web.dev: about 3 s of blocking for block, about 100 ms for fallback and optional. The column that matters is the third: once the webfont arrives after that window, the text never changes.
Why swap moves the layout
The fallback has different metrics: cap height, average character width, line height. When the webfont replaces it, a line can change height and everything below moves. On a large heading the shift is visible, on a 14 px paragraph it produces a small but real CLS.
Cut the gap, not just the time
- In the fallback's
@font-facedeclaresize-adjust,ascent-overrideanddescent-override: the text takes the same height before and after. - Preload only the woff2 used above the fold, not all 15:
as="font",type="font/woff2",crossorigin. - Keep the latin and latin-ext subsets: the other subsets are bytes nobody reads.
- Use
font-display: optionalfor decorative weights: if they arrive late, the page does not move.
Measuring the stylesheet
grep -c "@font-face" public/fonts.css
grep -c "font-display" public/fonts.css
du -cb public/fonts/*.woff2 | tail -1
The first number is how many blocks you declare, the second how many declare font-display: when the two differ, the browser applies auto to the ones you left out. The third is the on-disk weight of every file, 632,280 bytes in our case.
The limitation you have to name
font-display does not remove the shift: it decides when it happens. With block CLS stays low and the text is invisible for 3 s, which hurts reading and not the metric. Self-hosting only lowers the risk when the files come from the same origin with a long cache.
Where to continue
In the catalogue: typography components, gooey text morphing and animated text. The guides: free fonts for interfaces and fluid typography with clamp; the sibling article on CLS and the measured comparison of React libraries 2026.
Frequently asked questions
Is font-display mandatory?
No. Without a declaration the browser uses auto, which usually behaves like block for about 3 seconds. Declaring it makes the behaviour explicit.
swap or optional: which one?
swap when the family is part of the identity and has to appear; optional when the text matters more than the typeface and a second download is not worth a visit without the font.
Does font-display fix font CLS?
No, it changes when the text appears. The shift between fallback and webfont remains: size-adjust, ascent-override and a fallback with close metrics reduce it.
Should every weight be preloaded?
No. Preload removes one request from the queue, so it belongs on the single file used above the fold. Preloading 15 files takes bandwidth from CSS and images.
Do self-hosted fonts shift less?
Not directly. Dropping a third-party request removes DNS and connection time, so the font arrives sooner and the swap window closes sooner.