skip to content

A Next.js page still shows a visible text reflow shortly after load even though its typeface comes from `next/font/google`. Which layout-shift protections does next/font apply on its own, and where do they stop helping?

level: seniorimportance: should knowfreq 48%

answer

  1. two symptoms, two different mechanisms
  2. painting early is not the same as not moving
  3. make the fallback the same shape
  4. overridden ascent, descent, size-adjust

basics

~20 s

next/font defaults font-display to swap and, for Google fonts, generates a metric-adjusted fallback face so the fallback occupies almost the same space as the real font. Shift returns when you disable that adjustment, supply a local font without usable metrics, or load a face outside next/font.

solid answer

~50 s

Two things happen automatically. First, the generated `@font-face` uses `display: 'swap'` by default, so text paints immediately in a fallback and is repainted when the real face arrives — you get a flash, not invisible text. Second, and this is the part that removes the shift, next/font generates an extra fallback `@font-face` whose metrics are overridden — size, ascent, descent, line gap — so the local fallback renders at nearly the same dimensions as the web font, and the swap changes glyph shapes without moving the line boxes. That behaviour is the `adjustFontFallback` option, on by default for `next/font/google`. Reflow comes back when someone passes `adjustFontFallback: false`, when a `next/font/local` face lacks the metrics to compute a good adjustment, when the surrounding layout is genuinely width-sensitive so even small advance-width differences move things, or when the offending face — an icon font, a CMS-injected `<link>` — never went through next/font at all. This describes next/font in the App Router across Next 13.2 through 16.

code

typescript · 8 lines
typescript
import { Inter } from 'next/font/google'

// swap is the default; 'optional' trades typeface fidelity for zero swap shift.
export const inter = Inter({
  subsets: ['latin'],
  display: 'optional',
  fallback: ['system-ui', 'arial'],
})

go deeper

for a junior

Know that next/font sets font-display to swap by default, so text appears immediately in a fallback rather than being invisible while the font downloads.

for a middle

Explain the second mechanism: a generated fallback @font-face with overridden ascent, descent, line-gap and size-adjust so the fallback occupies nearly the same space, which is what stops the swap from moving the layout.

for a senior

Demonstrate the diagnosis — throttle, trace, attribute the shift to specific elements — and enumerate why the protection fails: fonts outside next/font, the adjustment disabled, weak local metrics, or width-sensitive layout.

for a principal

Own the policy. Decide whether brand fidelity or stability wins per surface, set the CLS budget the team is held to, and make sure no font can enter the product outside the pipeline that gives it an adjusted fallback.

## Two separate problems, two separate mechanisms Web-font loading causes two distinct symptoms and candidates blur them. **Invisible text (FOIT)** happens when the browser hides text while waiting for the font. That is a `font-display` question. **Layout shift** happens when text painted in one face is repainted in another with different metrics, so line breaks and block heights change and everything below moves. That is a *metrics* question. next/font addresses each separately, and only the second one is the interesting part. ## Mechanism one: font-display The generated `@font-face` carries `font-display: swap` unless you say otherwise, via the loader's `display` option, which accepts the CSS values `'auto' | 'block' | 'swap' | 'fallback' | 'optional'`. With `swap`, text is painted right away in the fallback and swapped when the real face loads — no blank period. That is good for perceived performance and bad for stability in isolation, because the swap is exactly the moment metrics change. `'optional'` is the alternative worth knowing: the browser gives the font a very short window, and if it does not arrive in time it uses the fallback for that page view and does not swap at all. That trades typographic fidelity on slow connections for a guaranteed absence of swap-induced shift. ## Mechanism two: the adjusted fallback face This is the part specific to next/font. Alongside the real `@font-face`, the build emits a second one for a local fallback family, carrying metric overrides derived from the actual font's metrics: ```css @font-face { font-family: '__Inter_Fallback_abc123'; src: local("Arial"); ascent-override: 90.20%; descent-override: 22.48%; line-gap-override: 0.00%; size-adjust: 107.40%; } ``` `size-adjust` scales the glyphs so the fallback's advance widths and x-height approximate the real font's; the ascent, descent and line-gap overrides make the line box the same height. The generated family stack lists the real font first and this adjusted fallback second. The consequence: during the pre-swap window text occupies almost exactly the space it will occupy afterwards, so the swap changes letterforms without moving anything. This is controlled by the `adjustFontFallback` option, on by default for `next/font/google`, and it is the single highest-value thing the feature does for Cumulative Layout Shift. ## Why shift can still appear Work through the causes in the order you would actually check them: 1. **The face did not come from next/font.** An icon font added by a component library, a `<link>` to a third-party font pasted into a marketing layout, a font referenced from CMS-authored HTML — none of these get an adjusted fallback. Grep the shipped HTML for external font hosts, and check whether the shifting element uses a family your loaders never declared. 2. **The adjustment was turned off.** Someone passed `adjustFontFallback: false`, usually while debugging something unrelated, and the fallback reverts to unscaled system metrics. 3. **A local font with poor metric data.** For `next/font/local` the adjustment depends on the file's own metrics; an aggressively subset or unusually built face can yield an approximation good enough for body copy but visibly off at display sizes. 4. **Layout that is genuinely width-sensitive.** A horizontal nav whose items are sized by text, a table with auto column widths, a single-line heading near a wrap boundary: here even a 1–2% advance-width difference changes wrapping or column widths, and a metric override that is close is not close enough. The fix is layout, not font config — fixed dimensions, `min-width`, or reserved space. 5. **The shift is not the font's at all.** Images without dimensions, late-injected banners, and ads produce the same visual symptom. Confirm attribution before touching font configuration: the browser's performance tooling attributes each layout-shift entry to the specific elements that moved, which tells you whether text is the culprit. ## Verifying rather than guessing A senior answer names how to check. Reproduce with the network throttled hard enough to widen the swap window, record a trace, and look at what moved and when. If the shift lands exactly at font-swap time and the moved elements are text blocks, it is a font problem. If it lands elsewhere, no amount of `adjustFontFallback` will help. Also compare the two candidate fixes explicitly: keeping `swap` plus adjusted fallbacks preserves the intended typeface at some risk of a small residual shift, whereas `display: 'optional'` eliminates the swap entirely but means slow connections simply never see your typeface. Which is right is a product decision — a brand-critical marketing page and an internal dashboard should not automatically get the same answer. ## The one-sentence version next/font makes the fallback *shaped like* the real font and paints immediately; it cannot help with fonts it does not control, adjustments you disabled, or layouts where even a near-perfect metric match still changes wrapping.

  • When would you choose `display: 'optional'` over the default `swap`?
    When a residual shift matters more than always showing the brand face. With `optional` the browser uses the web font only if it arrives within a very short window, otherwise it keeps the fallback for that page view and never swaps — zero swap-induced shift, at the price of some users never seeing your typeface. Reasonable for dense app UI, usually wrong for a brand landing page.
  • A `next/font/local` face still shifts noticeably at display sizes. What would you try?
    Check that the adjustment is enabled and that the fallback family it uses is one the target platforms actually have. Then reduce the layout's sensitivity: reserve space, fix heading line-height and box height, avoid text-sized horizontal layouts. If the residual mismatch is still visible on a hero, `display: 'optional'` on that face is a defensible trade.
  • How do you prove a given layout shift is caused by font swapping and not by something else?
    Throttle the network so the swap window widens, record a performance trace, and inspect the layout-shift entries — each names the elements that moved and when. A font-caused shift lands at swap time on text blocks. If the moved elements are images or late-injected banners, the fix is dimensions or reserved space, not font configuration.

saying these in an interview costs you the question

  • Says font-display: swap by itself prevents layout shift
  • Thinks next/font eliminates the flash of fallback text
  • Believes preloading the font removes the metric mismatch
  • Blames the font for shifts caused by unsized images
  • Turns off adjustFontFallback to 'simplify' the generated CSS

context