When to Use font-display: swap vs optional for Performance-Critical Web Typography
Determining the optimal font-display strategy requires balancing Cumulative Layout Shift (CLS) against First Contentful Paint (FCP). Typography delays typically stem from network latency and render-blocking font requests. This guide maps diagnostic thresholds directly to implementation rules. It extends Font-Display Values Explained; for broader architectural context, review Font Loading & Delivery Strategies before configuring critical path assets.
Problem Statement
Both swap and optional solve the flash of invisible text (FOIT) problem by rendering fallback text immediately, but they diverge sharply on what happens once the web font finishes downloading. swap always swaps the web font in — even minutes later — so a slow font guarantees a late layout shift if the fallback and web-font metrics differ. optional refuses to swap after its 100ms block window, trading a guaranteed-stable layout for the risk that first-time visitors on slow connections never see your branded type. Picking the wrong one means either shipping measurable CLS to every uncached visitor (swap without metric overrides) or silently dropping your brand font on the exact slow-network users who most need a fast, stable page (optional on body copy). This page resolves that single decision by mapping concrete CLS and FCP thresholds to one value.
Prerequisites
Before choosing a value, confirm the baseline so the decision is about render behaviour and not a misconfiguration:
- WOFF2 assets are served with
Content-Type: font/woff2andCache-Control: public, max-age=31536000, immutable, so theoptionalrepeat-visit path can hit cache. - Each
@font-facealready declares afont-displayvalue — an omitted value defaults toauto, which most engines treat likeblockand produces FOIT, not the FOUT you are tuning for. - You have a metric-matched fallback
@font-faceready (size-adjust,ascent-override,descent-override,line-gap-override) for theswappath; without it,swapcannot reach CLS < 0.1 on metric-divergent fonts. - You can throttle to Slow 3G in DevTools and read CLS from the Performance panel or a
layout-shiftPerformanceObserver, so the threshold rules below are measurable rather than guessed.
Browser Rendering Pipeline & Timeout Thresholds
The CSS Fonts specification defines two periods per font-display value: a block period (text is invisible while the font loads) and a swap period (fallback text is shown; the web font swaps in if it arrives within this window).
swaphas a 0ms block period and an infinite swap period. Text is immediately rendered in the fallback font. When the web font finishes downloading — even minutes later — it swaps in, which can cause a visible layout shift.optionalhas a 100ms block period and a 0ms swap period. If the font is already cached, it renders immediately. If it is not cached and does not arrive within the first 100ms, the browser may either skip it entirely or defer it to a future navigation depending on connection speed. This eliminates FOUT but means the custom font may never appear on first visit over a slow connection.
For full context on the other two values, block (3s block, infinite swap) and fallback (100ms block, 3s swap), see Font-Display Values Explained. The root cause of CLS spikes is an unbounded mismatch between fallback and custom font metrics when swap is used without metric overrides.
| Value | Block period | Swap period | First-visit FOUT | Late swap CLS | Best for |
|---|---|---|---|---|---|
swap |
0ms | infinite | Yes | Possible (mitigate with overrides) | Body copy, branded text that must appear |
optional |
100ms | 0ms | No | None | UI labels, decorative/icon fonts |
Diagnostic Steps:
- Run Lighthouse Performance audit > check 'Avoid large layout shifts' metric
- Open DevTools > Network > filter 'Font' > observe TTFB and Content Download
- Enable Rendering tab > check Layout Shift Regions
Decision rule: Use swap when text readability is critical and the custom font must always appear. Use optional when layout stability is the priority and you can accept the fallback being shown permanently on slow connections.
Diagnostic Workflow: DevTools & Core Web Vitals
Isolate font-induced CLS using the Chrome DevTools Performance panel. Record a trace during simulated 3G/4G throttling to identify layout shifts occurring after font swap. Cross-reference observed behavior with Font-Display Values Explained for precise timeout mapping.
Diagnostic Steps:
- Throttle network to Slow 3G in DevTools
- Record a Performance trace > inspect Layout Shift events in timeline
- Check the Initiator column in the Network tab to confirm which font requests triggered shifts
Action rules:
- If CLS > 0.1 and the font loads after 1s, add
size-adjust,ascent-override, anddescent-overrideto the fallback@font-facebefore switching away fromswap. - If FCP > 2.5s due to invisible text (FOIT), you are likely using
block— switch toswapand add<link rel="preload">for critical fonts. - If you need guaranteed layout stability regardless of network speed, use
optionalfor non-critical decorative fonts.
Implementation Matrix & Variable Font Integration
Map your typography hierarchy directly to display values. Apply swap to body copy for guaranteed readability. Use optional for UI components and icon sets where system-font substitution is acceptable.
Variable fonts benefit from swap paired with preload to leverage single-request efficiency across weight axes. Mitigate FOUT severity using size-adjust and ascent-override to align fallback metrics with the loaded font.
Diagnostic Steps:
- Audit CSS
@font-facedeclarations for missingfont-display - Validate that variable font
wghtaxis ranges matchfont-weightdescriptors in@font-face - Verify
preconnectto font origin appears in<head>before the first font request
Implementation: Apply font-display: swap to @font-face for primary text. Apply optional to secondary or decorative typefaces. Pair both with unicode-range subsetting to reduce payload.
Implementation: Critical Body Text with swap
For body copy that must always render in the brand face, declare swap and pair it with a metric-matched fallback so the inevitable late swap shifts no pixels. The primary block first, then a line-by-line read of why each declaration matters.
Body text @font-face with swap and a metric-matched fallback
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
/* Metric-matched fallback to minimise CLS during swap */
@font-face {
font-family: 'InterFallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 23%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'InterFallback', sans-serif;
}
The critical lines:
font-display: swapsets a 0ms block period, so text paints immediately in the next available family — never invisible, never FOIT.font-weight: 100 900declares the variable weight range on the real face; pairswapwith a variable file so a single request covers every weight, halving the windows during which a swap can shift layout.- The second
@font-facedefinesInterFallbackoverlocal('Arial')— a zero-network local font used only to host the metric overrides. It is the mechanism that makesswapsafe. size-adjust: 107%scales the fallback glyphs so its advance widths and x-height match Inter, holding line-break positions constant across the swap.ascent-override,descent-override, andline-gap-overridepin the fallback's vertical box to Inter's metrics, so line height does not jump when the real font lands — this is what drives swap-induced CLS toward zero. To derive these percentages, see calculating size-adjust for the system-ui fallback.- The
bodystack lists'Inter'first, then the override-bearing'InterFallback', then a generic — so the metric-matched fallback, not bare Arial, renders during the swap window.
Timeout & Error-Handling Variant: optional
For decorative type and UI labels where a missed brand font is acceptable, optional makes its own 100ms decision and never swaps late — the defensive pattern when layout stability outranks always showing the face. Combine it with a programmatic preload so cached repeat visitors still get the branded font inside the block window.
Decorative/UI font with optional plus a JS-driven cache warm
@font-face {
font-family: 'BrandIcons';
src: url('/fonts/icons.woff2') format('woff2');
font-display: optional;
}
// Warm the cache off the critical path so the NEXT navigation's
// optional block period (100ms) finds the font already available.
const font = new FontFace('BrandIcons', 'url(/fonts/icons.woff2)', { display: 'optional' });
font.load()
.then(() => document.fonts.add(font))
.catch(() => {
// optional already guaranteed a stable layout; just record the miss.
performance.mark('brandicons-load-failed');
});
With optional, the browser shows the fallback, gives the font 100ms to arrive, and if it misses, skips it for that page view without ever swapping — so there is no error path that can leave text invisible or shift layout. The FontFace.load() call here is purely a cache warm: it pulls the asset off the critical path so the next navigation finds it cached and renders the brand font instantly inside the block period. The .catch() records a miss via performance.mark() for RUM rather than altering rendering, because optional has already guaranteed the stable outcome.
Verification
Confirm each value behaves as intended under throttling, not on a warm local cache.
- Open DevTools → Network → filter Font, tick Disable cache, and throttle to Slow 3G. Reload.
- For
swap: watch the page paint fallback text immediately, then swap when the font row completes. Open the Performance panel, record the same load, and confirm the Layout Shift events after the swap sum to CLS < 0.1 — if not, the metric overrides are off. - For
optional: confirm that on the first uncached Slow 3G load the brand font does not appear (fallback persists), and that the Performance trace shows zero layout-shift events attributable to fonts. - Reload
optionala second time with cache enabled and confirm the brand font now renders within the first frame — proof the 100ms block period found the cached file. - Verify in the console with
document.fonts.check('1rem Inter')(true once loaded) and audit CLS in Lighthouse; a CI threshold of CLS < 0.1 flags any regression where someone removes the fallback overrides from aswapface.
Common Pitfalls
- Using
swapon heavy display fonts without metric-matching the fallback. Root cause: divergent ascent/descent and advance widths between Arial and a tall display face produce a reflow when the font swaps in. Fix: add a metric-matched fallback@font-facewithsize-adjustandascent-overridebefore shippingswap. - Applying
optionalto body text. Root cause:optionalskips the font after 100ms on slow networks, so first-time slow-connection users read the entire article in the fallback. Fix: reserveoptionalfor decorative/UI type; useswapplus overrides for body copy that must be branded. - Omitting
unicode-range. Root cause: the browser downloads the full multi-script payload even when the page renders only Latin glyphs, delaying the swap and widening the FOUT window. Fix: split subsets with unicode-range so only needed ranges fetch. - Treating
autoas a neutral default. Root cause: an omittedfont-displayresolves toauto, which most engines render asblock— producing FOIT, the opposite of the FOUT you are tuning. Fix: always set an explicit value on every@font-face. - Relying on
preloadalone withoutfont-display. Root cause: preload only advances discovery of the request; it does not change the block/swap timing, so FOIT can still occur. Fix: pair<link rel="preload">with an explicitfont-displayvalue.
FAQ
Does font-display: optional prevent FOUT entirely?
Yes, on uncached loads. If the font fails to load within the 100ms block period, the browser renders the fallback and never swaps for that page view, eliminating FOUT and CLS. On subsequent visits the font is cached and renders immediately.
When should swap be avoided?
Avoid swap without metric-matched fallbacks for decorative headings or icon fonts where the fallback character shapes are unrelated. Use optional or pair swap with size-adjust overrides to maintain layout stability.
How does optional interact with browser caching?
Cached fonts render immediately because they are available within the block period. optional only suppresses uncached fonts whose download would complete after the initial 100ms, making it highly effective for repeat traffic with proper long-term caching headers.
Can I mix swap and optional on the same page?
Yes, and you usually should. Apply swap per @font-face to the families that carry body copy and headings, and optional to icon sets, decorative display faces, or UI labels where the system fallback is acceptable. font-display is a per-@font-face descriptor, so each family resolves its own block and swap periods independently — there is no page-level setting to conflict.