Preventing FOUT and FOIT with font-display
Invisible text during load (FOIT) or a jarring glyph swap mid-read (FOUT) both come from the same source: the font-display descriptor governs how long the browser hides text waiting for a web font and whether it ever swaps. This page covers the block/swap period model, the ~3s block timeout, choosing swap versus optional, and using the Font Loading API to control the timing in JavaScript. It builds on Font Loading and Text Rendering and is part of Browser Rendering Pipeline Fundamentals.
The Three Periods
Every web font load is governed by three time windows. During the block period the browser renders text in an invisible fallback β space is reserved but no glyphs appear (this is FOIT). During the swap period the browser shows a visible fallback and will swap to the web font the instant it arrives (this is FOUT). After the failure period the browser gives up and uses the fallback permanently. font-display sets the length of the block and swap periods.
/* Reproduction: default behavior is block β text is invisible up to ~3s */
@font-face {
font-family: 'Merriweather';
src: url('/fonts/merriweather.woff2') format('woff2');
/* no font-display β 'auto' β block period of ~3s on a slow connection */
}
On a slow network the heading set in Merriweather renders nothing at all until the font arrives or three seconds elapse β a blank gap exactly where the most important text should be. That blank window delays the largest text node and inflates Largest Contentful Paint.
The 3-Second Block Timeout
The block value (and auto, which most engines treat as block) imposes a block period that Chrome and Firefox cap at roughly 3 seconds. For that entire window, affected text is invisible. If the font arrives at 2.9s, the user stared at empty space for 2.9s for no benefit. If it arrives at 3.1s, the browser has already locked in the fallback, then swaps anyway β the worst of both behaviors. This timeout is why block should never apply to above-the-fold content.
[Render timeline β font-display: block, slow 3G]
0ms | FCP fires, but text nodes in Merriweather paint NOTHING
0β3000ms | block period β invisible text, layout space reserved
2900ms | woff2 arrives β text finally paints in web font
LCP | 2900ms (text node) β fails the 2.5s budget
Choosing swap vs optional
/* Fix A β swap: paint fallback at 0ms, swap when the font loads */
@font-face {
font-family: 'Merriweather';
src: url('/fonts/merriweather.woff2') format('woff2');
font-display: swap; /* block period 0ms β no invisible text, ever */
}
/* Fix B β optional: use the font only if it is near-instant (cached) */
@font-face {
font-family: 'Merriweather';
src: url('/fonts/merriweather.woff2') format('woff2');
font-display: optional; /* ~100ms block, 0ms swap β no late swap, no CLS */
}
swap guarantees the user can read immediately and always eventually sees the web font, at the cost of a visible swap and a layout shift unless fallback metrics are matched. optional gives the browser a ~100ms window to use the font and a zero-length swap period: if the font is not ready in time, the browser commits to the fallback for that page view and never swaps. That means optional cannot cause a font-driven layout shift after first paint, which is its main appeal.
| Value | Block | Swap | Late swap? | CLS risk |
|---|---|---|---|---|
block |
~3s | infinite | yes | high (and FOIT) |
swap |
0ms | infinite | yes | high without metric overrides |
fallback |
~100ms | ~3s | only within 3s | medium |
optional |
~100ms | 0ms | never | none |
Use swap for content fonts you always want shown, paired with the metric overrides in Reducing layout shift from web fonts. Use optional for decorative or secondary fonts where a cached-only policy is acceptable and zero CLS is the priority.
Forcing Load with the Font Loading API
font-display is declarative; the Font Loading API gives imperative control. You can kick off a font fetch before any element matches it, and gate a render on completion to avoid the swap entirely for a critical block.
// Start the fetch immediately, independent of render-tree matching
const merri = new FontFace(
'Merriweather',
'url(/fonts/merriweather.woff2) format("woff2")',
{ display: 'swap' }
)
document.fonts.add(merri)
// load() returns a promise resolving when glyphs are decoded and ready
merri.load().then(() => {
document.documentElement.classList.add('fonts-loaded') // reveal styled text
})
// Or wait on a specific font + size before painting a critical heading
document.fonts.load('700 2rem Merriweather').then(() => {
performance.mark('heading-font-ready')
})
Gating a single critical heading on document.fonts.load() lets you keep swap globally while ensuring the one element most sensitive to a mid-read swap is rendered correctly the first time. Avoid gating the whole page β that reintroduces FOIT manually.
Verification
Confirm there is no invisible-text window and that the swap policy behaves as configured:
document.fonts.ready.then(() => {
const fcp = performance.getEntriesByName('first-contentful-paint')[0]
const ready = performance.getEntriesByName('heading-font-ready')[0]
if (ready && fcp && ready.startTime - fcp.startTime > 100) {
console.warn('Font readiness lags FCP β consider preload')
}
})
If the font readiness mark trails FCP by more than ~100ms, the fetch is starting too late; add a preload so the request begins during HTML parse rather than after render-tree matching. Because the preload scanner never discovers fonts referenced only in CSS @font-face rules, an explicit <link rel="preload" as="font"> is the only way to start the request before the CSSOM resolves.
Frequently Asked Questions
What is the difference between FOUT and FOIT?
FOIT (Flash of Invisible Text) is when the browser hides text during the block period while it waits for a web font β the reader sees blank space where glyphs should be. FOUT (Flash of Unstyled Text) is when the browser paints a fallback font immediately and then swaps to the web font once it loads, causing a visible restyle. font-display: block produces FOIT; font-display: swap produces FOUT.
Which font-display value should I use for body text?
Use swap for body and heading text you always want rendered in the web font, so readers never face invisible text and Largest Contentful Paint is not delayed. Pair it with size-adjust and ascent-override metric overrides to neutralize the layout shift the swap would otherwise cause. Reserve optional for decorative fonts where a cached-only policy and zero CLS matter more than always showing the font.
Why does font-display optional never cause layout shift?
optional gives the font a short block period of roughly 100ms and a zero-length swap period. If the font is not decoded within that window, the browser commits to the fallback for the entire page view and will not swap even after the font finishes loading. With no post-paint swap, there is no font-driven reflow, so no layout-shift entry is recorded after first paint.
Does preloading a font change how font-display behaves?
No. Preloading starts the network request earlier but the block, swap, and failure periods are still governed entirely by the font-display descriptor. Preload simply increases the chance the font arrives before the block period ends, which under optional is the difference between using the web font and permanently keeping the fallback.
When should I use the Font Loading API instead of font-display?
Use the Font Loading API when you need imperative control the declarative descriptor cannot give: starting a fetch before any element matches the font, or gating a single critical element on document.fonts.load() so it renders correctly the first time while the rest of the page keeps swap. For most fonts, font-display alone is sufficient.
Related Guides
- Font Loading and Text Rendering β the parent overview of how fonts enter the rendering pipeline.
- Reducing layout shift from web fonts β the metric-override recipe that makes
swapsafe. - How the preload scanner speculatively loads resources β why CSS-referenced fonts need an explicit preload.
- Using fetchpriority and preload for LCP images β the same early-fetch pattern applied to the LCP element.