Font Loading and Text Rendering
Web fonts sit on the critical path twice: once as a network resource the browser must fetch before it can paint final text, and again as a layout input whose metrics differ from the fallback, causing text to reflow when the real font swaps in. This topic covers how @font-face fetch timing, font-display, FOUT/FOIT behavior, preload, and metric overrides shape both First Contentful Paint and Cumulative Layout Shift. It is part of Browser Rendering Pipeline Fundamentals.
A web font fetch does not start when the browser sees the @font-face rule β it starts when the render tree first matches an element to that font family. By then the CSSOM is already built, the DOM is parsed, and layout is about to run. The font request therefore races against first paint, and the loser of that race is visible to the user as either invisible text or a flash of swapped glyphs.
When the Fetch Starts
The font request is lazy by design. The sequence is: parse HTML into DOM nodes, build the CSSOM, generate the render tree, and only when a render-tree nodeβs computed font-family resolves to a declared @font-face does the browser queue the download. This avoids fetching fonts that no rendered element uses, but it pushes the request to the worst possible moment β after the CSSOM round-trip is already paid.
<!-- Without preload: fetch waits for CSSOM + render tree + font match -->
<link rel="stylesheet" href="/css/app.css"> <!-- declares @font-face -->
<!-- With preload: fetch starts during HTML parse, in parallel with CSS -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font"
type="font/woff2" crossorigin> <!-- crossorigin is mandatory for fonts -->
<link rel="stylesheet" href="/css/app.css">
Preload moves the request forward by a full round-trip, but it does not change the paint policy β that is font-displayβs job. The crossorigin attribute is required even for same-origin fonts because font fetches are always made in CORS mode; omitting it causes a duplicate, non-preloaded request.
FOUT vs FOIT
Two failure modes describe what the user sees while a font is loading. FOIT (Flash of Invisible Text) hides text entirely until the font arrives β the layout reserves space but renders nothing. FOUT (Flash of Unstyled Text) paints fallback text immediately, then re-renders in the web font once it loads. FOIT delays content visibility; FOUT shows content sooner but introduces a visible swap and usually a layout shift. The font-display descriptor selects which behavior you get, with a default of block that produces FOIT.
| font-display | block period | swap period | user-visible result |
|---|---|---|---|
auto |
up to ~3s | infinite | engine default, usually FOIT |
block |
~3s | infinite | FOIT, then swaps to web font |
swap |
0ms | infinite | FOUT, fallback shown immediately |
fallback |
~100ms | ~3s | brief FOIT, then fallback locks if late |
optional |
~100ms | 0ms | font used only if cached/near-instant |
The choice between these is detailed in Preventing FOUT and FOIT with font-display. The short rule: swap for body text where reading speed matters, optional for fonts whose absence is cosmetically acceptable, and never the default block for above-the-fold copy.
Fonts as a Render-Blocking and Layout-Shift Source
A font does not block the first paint the way a stylesheet does β the browser will paint fallback text under swap. But under the default block, text inside the affected elements is invisible for up to three seconds, which directly delays the largest text node and can wreck Largest Contentful Paint. And whenever the swap fires, the fallback and web font almost never share the same cap-height, x-height, and advance widths, so lines re-wrap and blocks change height. That movement is recorded by the layout instability algorithm as Cumulative Layout Shift.
[Main Thread β font swap on body copy]
0ms | FCP β fallback (Arial) painted, layout height = 1840px
1180ms | Inter woff2 arrives, render-tree nodes re-matched
1182β1191ms | Recalculate Style + Layout (9ms) β block reflows to 1792px
1191ms | Paint β 48px upward shift, CLS += 0.07
A 0.07 shift from a single font swap is enough to fail the 0.1 CLS threshold once other shifts are added. Reducing layout shift from web fonts covers the metric-override techniques that collapse this reflow to zero, and you can watch the shift land live with the Layout Instability API.
Aligning Fallback Metrics
The reflow on swap exists because the fallback font occupies a different amount of vertical and horizontal space than the web font. The @font-face metric overrides let you reshape a fallback so it matches the web fontβs box, eliminating the geometry delta the swap would otherwise cause.
/* Before: fallback Arial is shorter and narrower than Inter β swap reflows */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
}
/* After: a metric-matched fallback that occupies Inter's exact box */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%; /* scale glyphs so x-height matches Inter */
ascent-override: 90%; /* pin the ascent so line boxes are identical */
descent-override: 22%; /* pin the descent for matching line height */
line-gap-override: 0%; /* remove extra leading the fallback would add */
}
Setting font-family: 'Inter', 'Inter Fallback', sans-serif then renders fallback text in a box that is dimensionally identical to the real font. When Inter swaps in, glyph shapes change but no box changes size, so the swap produces zero layout shift. This is the same principle behind the framework βfont fallbackβ tooling β Next.js next/font and Fontaine compute these overrides automatically.
Validation
Confirm the fetch starts early and the swap costs nothing with the Font Loading API and a layout-shift observer:
// Fires once the web font is usable β measure against FCP
document.fonts.ready.then(() => {
performance.mark('fonts-ready')
})
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// recent-input shifts are user-driven; only unexpected shifts count
if (!entry.hadRecentInput && entry.value > 0) {
console.warn('Layout shift:', entry.value.toFixed(4))
}
}
}).observe({ type: 'layout-shift', buffered: true })
| Metric | Target | How measured |
|---|---|---|
| Font request start | During HTML parse | Network panel initiator = preload |
document.fonts.ready |
< 1.0s on Fast 4G | User Timing mark vs FCP |
| CLS from font swap | 0.00 | layout-shift entries during swap window |
| LCP (text node) | < 2.5s | Web Vitals overlay |
Cross-check these against the broader critical-path budget in Critical Rendering Path Optimization: a font that preloads early but still swaps with a visible reflow means the metric overrides, not the fetch timing, are the remaining problem.
The Three Font Timeline Periods
Every web font load is governed by three periods, and font-display sets their lengths. During the block period the browser reserves space but renders text invisibly, waiting for the font β this is the Flash of Invisible Text (FOIT). During the swap period it renders a fallback and will swap to the web font the instant it arrives β the Flash of Unstyled Text (FOUT). After the failure period it gives up and keeps the fallback permanently. The font-display values map directly onto these: block imposes a long block period (FOIT), swap uses a zero block period and an infinite swap period (FOUT), fallback gives a tiny block period and a short swap, and optional gives a tiny block period with no swap β if the font is not ready in time, the fallback is used for the whole page view. Choosing the value is really choosing which trade-off you can live with, and preventing FOUT and FOIT with font-display works through each.
For above-the-fold content the choice matters most, because a long block period means users stare at blank space while the font loads. swap guarantees text is always visible but accepts a visible restyle when the font arrives; optional accepts that some users never see the web font in exchange for zero post-paint reflow. The one value to avoid on critical text is the default auto, which most engines treat as block and which can hide text for up to three seconds on a slow connection. Preloading the font shortens the odds that it arrives before the block period ends, but it does not change the period lengths β only font-display does that.
Font Swaps Cause Layout Shift
The performance sting of web fonts is not just invisible text β it is the layout shift a swap produces after first paint. A fallback font and the web font almost always have different glyph metrics: different advance widths, line heights, and cap heights. When the browser swaps the web font in, it re-measures every line using it, box heights change, and following content moves β a shift the compositor scores as Cumulative Layout Shift because it happens after the content was already painted. The larger the metric mismatch and the more text affected, the worse the shift.
The modern fix is to make the fallback occupy the same space as the web font so the swap moves nothing. The size-adjust, ascent-override, descent-override, and line-gap-override descriptors on an @font-face let you tune a fallbackβs metrics to match the web font, so that when the swap happens the boxes are already the right size. This is the technique behind reducing layout shift from web fonts: define a metric-adjusted fallback, render it during the swap period, and the eventual swap changes glyph shapes without changing geometry. Combined with font-display: optional on non-critical text, which forgoes the swap entirely, it is possible to ship web fonts with essentially zero font-driven CLS.
/* A metric-adjusted fallback so the swap to the web font shifts nothing. */
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%; /* tune so line boxes match the web font */
ascent-override: 90%;
descent-override: 22%;
}
body { font-family: "Inter", "Inter Fallback", sans-serif; }
A Font Strategy That Ships Zero CLS
Putting the pieces together yields a repeatable strategy for shipping web fonts without the two costs β invisible text and layout shift. For critical, above-the-fold text, use font-display: swap so text is always visible, and pair it with a metric-adjusted fallback (via size-adjust and the override descriptors) so the eventual swap changes glyph shapes without moving any boxes. Preload the critical font so it is more likely to arrive during the swap period. For non-critical text where you can accept that some users keep the fallback, font-display: optional forgoes the swap entirely and therefore produces no post-paint reflow at all. The combination targets zero font-driven CLS while keeping text readable throughout the load.
The verification is a Performance and Layout Instability trace: after the strategy is in place, the font swap should produce no layout-shift entries, because the metric-matched fallback already reserved the correct space. If a shift still appears, the fallback metrics are not close enough and need tuning, or a section is using the default auto/block behaviour and hiding text. Treating the fontβs contain-intrinsic-style metric adjustments as data that travels with the font β updated when the web font or its fallback changes β keeps the zero-CLS property from silently regressing. Done once and maintained, it removes fonts from the list of things that cause layout instability, which is otherwise one of the most common sources of a failing CLS score. Subsetting the font to the characters the page actually uses further shortens the download, raising the odds the font arrives during the swap period rather than after it, and reducing the bytes on the critical path. Self-hosting the font rather than fetching it from a third-party origin removes a cross-origin connection setup from the load, which on a slow first visit can be the difference between the font making the swap window and missing it. These delivery choices compound with the font-display and metric-matching strategy: the descriptor decides how a late font behaves, and the delivery decides how often the font is late in the first place.
Frequently Asked Questions
Why does a web font download start so late even though the @font-face rule is in the CSS?
The fetch is lazy. The browser only queues the download once a render-tree nodeβs computed font-family actually resolves to that @font-face, which happens after HTML parsing, CSSOM construction, and render tree generation are complete. Use <link rel="preload" as="font" crossorigin> to start the request during HTML parse instead.
Does a web font block first paint the way a stylesheet does?
Not the first paint itself. Under font-display: swap the browser paints fallback text immediately. Under the default block, text in the affected elements stays invisible for up to about three seconds, which delays the largest text node and can hurt Largest Contentful Paint even though it does not block the initial frame.
Why is crossorigin required on a font preload even for same-origin fonts?
Font fetches are always made in CORS mode. If the preload omits crossorigin, the preloaded resource does not match the later CORS-mode font request, so the browser discards the preload and issues a second, duplicate download.
How do I make a font swap cause zero layout shift?
Define a metric-matched fallback @font-face using size-adjust, ascent-override, descent-override, and line-gap-override so the fallback occupies the web fontβs exact box. When the real font swaps in, glyph shapes change but no box resizes, so layout shift from web fonts drops to 0.00.
Which font-display value should I use for body text?
Use swap for body copy where reading speed matters, so text is visible immediately. Reserve optional for fonts whose absence is cosmetically acceptable, and avoid the default block above the fold. The trade-offs are covered in Preventing FOUT and FOIT with font-display.
Related Guides
- Preventing FOUT and FOIT with font-display β how each
font-displayvalue maps to a paint policy and which to pick. - Reducing layout shift from web fonts β the metric-override recipe that collapses swap reflow to zero.
- Critical Rendering Path Optimization β where font fetch timing sits in the overall first-paint budget.
- Browser Rendering Pipeline Fundamentals β the parent overview of every stage from parse to paint.