Stacks of secondhand books piled on tables in a crowded bookshop

Build Note

next/font: why display optional beats swap

A late font re-wrapped the hero at 375px and cost 0.076 CLS. Keeping the size-adjusted fallback for the whole page view removes the swap instead of shrinking it.

Published August 5, 2026

Use display: "optional" rather than swap for self-hosted fonts. If the font misses its short block window, the size-adjusted fallback is kept for the whole page view instead of swapping in late and re-measuring the text — which is what turns a headline into a two-line headline and puts real shift into your field data.

The failure this fixes

A late-arriving web font changes the width of every glyph on the page. Where text wraps, that can add or remove a line, and everything below moves. Lab tests rarely catch it — by the time you profile, the font is cached and swaps instantly — so the symptom is a Cumulative Layout Shift number that looks fine locally and is not fine in the field.

The measurement on this site was specific: with swap, a late font re-wrapped the hero H1 at 375px viewports, adding a line and producing a 0.076 shift for slow-connection visitors. That is most of the way to the 0.1 threshold on a single element, on the page that matters most, for exactly the users already having the worst time.

font-display strategies compared by what happens when the font is slow
swapoptional
Block periodVery shortVery short
If the font is lateSwaps in whenever it arrivesFallback kept for this page view
Layout shift riskReal, and worst on slow connectionsNone after first paint
First visit, slow networkBrand font, with a reflowFallback, no reflow
Repeat visitsBrand fontBrand font (cached)
What you're optimisingBrand fidelity on every visitStability on every visit

The configuration

Self-host through next/font so the files come from your own origin — that also means no third-party font request to allow through a strict Content-Security-Policy. Set display and preload explicitly rather than relying on defaults.

app/layout.tsx
/* display: "optional" (not "swap") is what keeps field CLS at 0: if a font
   misses the ~100ms block window, the size-adjusted fallback is kept for the
   whole page view — text is never re-measured mid-visit. With swap, late
   fonts re-wrapped the hero H1 at 375px viewports (+1 line, a 0.076 shift
   for slow-connection users). Preload + self-hosting means fast and repeat
   visits still always render the brand fonts. */
const interTight = Inter_Tight({
  subsets: ["latin"],
  variable: "--font-inter-tight",
  display: "optional",
  preload: true,
})

Three families are declared this way and exposed as CSS variables on <html>, so Tailwind can reference them without any runtime font loading logic. Nothing about this costs JavaScript: next/font generates the @font-face rules and the preload links at build time.

Why not just tune the fallback?

Because metric overrides get you close, and close still re-measures. Adjusting the fallback's size and spacing so it occupies almost the same space is genuinely useful — it is what makes optional tolerable to look at — but as a fix for shift it only shrinks the number. On a large headline at a narrow viewport, a small per-glyph difference is enough to change where the line breaks, and one extra line is not a small shift.

The stronger move is to remove the swap event rather than minimise its consequences. That is the difference between a metric that is usually under the threshold and one that is structurally zero, which is the same distinction behind sizing an animated headline from its longest state.

What else has to hold for CLS to stay at zero

Fonts are one of four usual causes and the least visible. The others are media without dimensions, content injected above existing content after paint, and anything whose height is decided by data that arrives late. Every image needs explicit width and height; every embed, banner, and async block needs its space reserved before it renders.

Handled together, zero is a normal result rather than an exceptional one — and it is the one Core Web Vital you can genuinely make structural rather than merely good. The other two take different work entirely.

Font strategy is decided once, early, and quietly determines a metric you will otherwise chase for months — one of several defaults set at the start of a build.

Questions

Won't some visitors never see the brand font?

Some first-time visitors on slow connections, yes — they get the size-adjusted fallback for that page view, and the real font on the next one once it's cached. That's the trade: a small number of visits render in a metrically matched substitute instead of every slow visit getting a reflow.

Why not use font-display: swap with a size-adjusted fallback?

That's the usual advice and it does shrink the shift rather than remove it. Metric overrides get the fallback close, not identical, so a late swap still re-measures the text. On a headline at a narrow viewport, close is enough to add a line. Optional removes the swap entirely for the visits at risk.

Does this apply to icon fonts?

Don't use one. An icon font is a render-blocking dependency whose failure mode is boxes or invisible glyphs, and inline SVG costs nothing at request time and can't shift. If you've inherited one, the same reasoning applies with more urgency.

How do I verify it worked?

Lab CLS is the wrong instrument here — it usually shows 0 either way, because the font is cached by the time you test. Throttle to a slow connection with an empty cache and watch the headline, then confirm on field data, which is where the shift was showing up in the first place.

Written by

Karol

Senior engineer and systems architect behind Tall Karol. Everything published here is grounded in real client work — no roundups, no tools that haven't run in production.

Why Tall KarolWork with Tall Karol

Related notes

More on web development

Related service: Web Development

Want this kind of engineering on your project?

Tall Karol takes on fractional and project-based engagements for startups and agencies.

Book a working session