Font Loading Performance (font-display)
Custom web fonts loaded with @font-face are not available the instant a page starts rendering — the browser has to download the font file before it can paint text in that font. What happens to your text while that download is in flight is controlled almost entirely by one CSS property: font-display. Get it wrong and readers either stare at blank space or watch text visibly jump as fonts swap in.
FOIT vs FOUT
Historically, browsers handled a missing web font in one of two ways. Some hid the text entirely until the font arrived — a Flash Of Invisible Text (FOIT). Others immediately rendered the text in a fallback system font, then swapped it once the web font loaded — a Flash Of Unstyled Text (FOUT). Neither behavior was consistent across browsers, and neither was under the developer's control until font-display was introduced.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap; /* <-- controls FOIT/FOUT behavior */
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}The font-display Values
Value | Block period | Swap period | Behavior |
|---|---|---|---|
auto | Browser default | Browser default | Leaves the decision entirely to the browser (usually behaves like block). |
block | Short (~3s) | Infinite | Text stays invisible briefly, then swaps in whenever the font finishes loading — classic FOIT. |
swap | None (~0s) | Infinite | Fallback text shows immediately, swaps to the web font whenever it loads — classic FOUT. |
fallback | Very short (~100ms) | Short (~3s) | Brief invisible period, then fallback text; if the web font is not ready within the swap window it is never swapped in for that render. |
optional | Very short (~100ms) | None | Almost never swaps — if the font is not cached/ready nearly instantly, the fallback is used for the rest of the page's life. |
Each value is really describing two timers: a block period, during which invisible text is tolerated while waiting for the font, and a swap period, during which the browser will still swap to the web font once it arrives if it finishes downloading late. Once both periods expire, the browser commits to whichever font it is currently showing.
swap as a Practical Default
For most content sites, font-display: swap is the commonly recommended default. It guarantees text is never invisible — readers always see something immediately in a fallback font — at the cost of a visible font swap and the layout shift that can come with it if the web font has different metrics (character widths, line height) than the fallback.
/* Recommended baseline for most body text */
@font-face {
font-family: 'Source Sans';
src: url('/fonts/source-sans.woff2') format('woff2');
font-display: swap;
}
/* For a purely decorative/logo font where a swap would look jarring,
optional avoids the swap entirely by falling back permanently
on first (uncached) visits */
@font-face {
font-family: 'Brand Display';
src: url('/fonts/brand-display.woff2') format('woff2');
font-display: optional;
}Complementing font-display with Preload
font-display only controls what happens while a font is loading — it does nothing to make the font load faster. To get the critical font file requested as early as possible (rather than discovered only after the CSS has been parsed), pair it with an HTML-level resource hint:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
Preloading tells the browser to start fetching the font in parallel with other critical resources instead of waiting to discover it inside a stylesheet. Combined with font-display: swap, this gives text the best chance of already having the web font available by the time the block/swap window would otherwise expire, minimizing both invisible text and visible swapping.