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.

swap vs optional decision tree A decision tree choosing between font-display swap and optional based on whether the custom font must always appear and whether layout stability is the top priority. Must the custom font always appear? yes no Is CLS from swap acceptable / fixable? Layout stability is the top priority? yes no yes swap + metric overrides swap add size-adjust first optional fallback may persist swap = readability first; optional = zero swap-CLS, may skip font on slow nets.
Pick swap when the branded font must always render; pick optional when layout stability outranks always showing it.

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/woff2 and Cache-Control: public, max-age=31536000, immutable, so the optional repeat-visit path can hit cache.
  • Each @font-face already declares a font-display value — an omitted value defaults to auto, which most engines treat like block and produces FOIT, not the FOUT you are tuning for.
  • You have a metric-matched fallback @font-face ready (size-adjust, ascent-override, descent-override, line-gap-override) for the swap path; without it, swap cannot 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-shift PerformanceObserver, 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).

  • swap has 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.
  • optional has 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, and descent-override to the fallback @font-face before switching away from swap.
  • If FCP > 2.5s due to invisible text (FOIT), you are likely using block — switch to swap and add <link rel="preload"> for critical fonts.
  • If you need guaranteed layout stability regardless of network speed, use optional for 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-face declarations for missing font-display
  • Validate that variable font wght axis ranges match font-weight descriptors in @font-face
  • Verify preconnect to 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: swap sets a 0ms block period, so text paints immediately in the next available family — never invisible, never FOIT.
  • font-weight: 100 900 declares the variable weight range on the real face; pair swap with a variable file so a single request covers every weight, halving the windows during which a swap can shift layout.
  • The second @font-face defines InterFallback over local('Arial') — a zero-network local font used only to host the metric overrides. It is the mechanism that makes swap safe.
  • 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, and line-gap-override pin 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 body stack 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.

  1. Open DevTools → Network → filter Font, tick Disable cache, and throttle to Slow 3G. Reload.
  2. 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.
  3. 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.
  4. Reload optional a 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.
  5. 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 a swap face.

Common Pitfalls

  • Using swap on 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-face with size-adjust and ascent-override before shipping swap.
  • Applying optional to body text. Root cause: optional skips the font after 100ms on slow networks, so first-time slow-connection users read the entire article in the fallback. Fix: reserve optional for decorative/UI type; use swap plus 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 auto as a neutral default. Root cause: an omitted font-display resolves to auto, which most engines render as block — producing FOIT, the opposite of the FOUT you are tuning. Fix: always set an explicit value on every @font-face.
  • Relying on preload alone without font-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 explicit font-display value.

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.

Related