
How to Load Web Fonts Efficiently with font-display
Custom fonts give a site its personality, but they come with a loading problem. A font file is a separate download that often isn't discovered until the browser has parsed your CSS and found text that needs it. Until the file arrives, the browser has to decide what to do with that text: hide it, or show it in a fallback font and swap later. Get this wrong and you end up with a blank page where the headline should be, or text that visibly jumps and reflows a second after load.
The font-display descriptor is how you control that decision. This guide explains each value, when to use which, and how to combine font-display with preloading, subsetting, and fallback metric overrides so fonts load quickly without hurting your Largest Contentful Paint or Cumulative Layout Shift scores.
FOIT and FOUT
Two acronyms describe the two failure modes:
- FOIT (Flash of Invisible Text): the browser hides text while the web font downloads. Users see empty space where content should be.
- FOUT (Flash of Unstyled Text): the browser shows text in a fallback font immediately, then swaps to the web font when it arrives. Users can read right away, but the text may shift when the swap happens.
Neither is free. FOIT delays reading; FOUT can cause layout shift. font-display lets you choose the trade-off per font.
The Font Display Timeline
Every font load moves through up to three periods:
- Block period: if the font isn't loaded, text is rendered invisibly using a fallback. If the font arrives during this period, it's used straight away.
- Swap period: if the font isn't loaded, text is rendered in the fallback font. If the font arrives, the browser swaps it in.
- Failure period: the browser gives up on the web font for this page and keeps the fallback.
The font-display values are just different lengths for those periods.
The font-display Values
You set font-display inside an @font-face rule:
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
auto
Lets the browser decide. In practice most browsers behave like block. This is the default if you don't specify anything.
block
A short block period, usually around 3 seconds, followed by an unlimited swap period. Text is invisible for up to three seconds, then shows in the fallback until the font loads. Use it only for fonts where the fallback would be meaningless, like an icon font, where showing the underlying characters would be confusing.
swap
An extremely short block period (effectively zero) and an unlimited swap period. Text appears immediately in the fallback and swaps whenever the font loads, even if that's much later. This is the value most commonly recommended for body and heading fonts because text is never invisible. The downside is that a late swap can shift the layout.
fallback
An extremely short block period (around 100ms) and a short swap period (around 3 seconds). If the font doesn't arrive within that window, the fallback stays for the rest of the page view. The font is still downloaded and cached, so it'll be used on the next page. This is a good compromise for body text: users see content quickly, and late swaps that would cause jarring shifts are avoided.
optional
An extremely short block period and no swap period. If the font isn't available almost immediately, the browser uses the fallback for this page view and doesn't swap later. Browsers may also skip the download entirely on slow connections. Once cached, the font is used on subsequent pages.
optional gives the best layout stability, since there's never a late swap, and pairs very well with preloading. It's ideal when the font is a nice-to-have rather than essential to your brand.
Quick Comparison
| Value | Block period | Swap period | Best for |
|---|---|---|---|
block | Short (about 3s) | Infinite | Icon fonts |
swap | Extremely short | Infinite | Headings, brand-critical text |
fallback | Extremely short (about 100ms) | Short (about 3s) | Body text |
optional | Extremely short | None | Performance-first sites |
Exact timings are left to the browser, so treat the numbers as approximate.
Using font-display with Google Fonts
Google Fonts adds font-display for you through the display URL parameter:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap"
rel="stylesheet"
/>
The preconnect hints start the connection to Google's servers early. That helps, but a third-party font still requires extra connections. Self-hosting the files on your own domain is usually faster, and it gives you full control over caching and preloading.
Preload the Fonts That Matter
The browser normally discovers fonts late: it has to download the HTML, then the CSS, then build the render tree and find text that uses the font. Only then does the font request start. A preload hint moves that request to the very beginning:
<head>
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="stylesheet" href="/css/main.css" />
</head>
Three things to get right:
crossoriginis required, even for fonts on your own domain. Fonts are fetched in anonymous CORS mode, and without the attribute the preloaded file won't match the real request, so it's downloaded twice.- The URL must match exactly the one in your
@font-facerule. - Preload only one or two files, typically the regular weight used for body text and maybe the heading font. Preloading every weight and style competes with more important resources like your CSS and hero image.
Preload combined with font-display: optional is a strong pattern. The preload makes it likely the font arrives within the tiny block window, and optional guarantees there's never a late swap if it doesn't.
Use WOFF2 and Subset Your Fonts
The smaller the file, the sooner it arrives.
Serve WOFF2
WOFF2 uses Brotli-based compression and is supported in every modern browser. There's no reason to ship TTF, OTF, or EOT to modern browsers anymore:
@font-face {
font-family: "Source Serif";
src: url("/fonts/source-serif-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: fallback;
}
Subset to the Characters You Need
A full font file might include thousands of glyphs for languages your site never displays. Subsetting removes them. The pyftsubset tool from fonttools is a common choice:
pip install fonttools brotli
pyftsubset SourceSerif-Regular.ttf \
--unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" \
--layout-features="kern,liga" \
--flavor=woff2 \
--output-file=source-serif-latin.woff2
That keeps Basic Latin, Latin-1, and common punctuation and symbols, which covers most English and Western European text.
Split with unicode-range
If you do need several scripts, split the font into subsets and declare each with unicode-range. The browser downloads only the subsets whose characters actually appear on the page:
@font-face {
font-family: "Source Serif";
src: url("/fonts/source-serif-latin.woff2") format("woff2");
font-display: fallback;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC, U+2122;
}
@font-face {
font-family: "Source Serif";
src: url("/fonts/source-serif-cyrillic.woff2") format("woff2");
font-display: fallback;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
An English page never requests the Cyrillic file.
Stop the Layout Shift: Tune Your Fallback
swap and fallback keep text visible, but the swap itself can shift content if the fallback font has different proportions. A paragraph set in Arial might wrap onto four lines, then five lines after Inter arrives, pushing everything below it down.
You can make the fallback match the web font closely using @font-face metric overrides:
size-adjustscales the fallback's glyphs.ascent-override,descent-override, andline-gap-overrideadjust vertical metrics so line heights match.
The trick is to create a new font family that points at a local system font and adjusts it:
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: "Inter", "Inter Fallback", system-ui, sans-serif;
}
The percentages above are illustrative. The correct values depend on the specific fonts. Tools such as Fontaine, the Capsize library, and online fallback font generators calculate them from the font files. Frameworks do this automatically too: Next.js's next/font generates an adjusted fallback for you when you load fonts through it.
With a well-tuned fallback, a swap changes the shape of the letters but barely moves the text, so layout shift is close to zero.
Metric overrides are supported in Chromium-based browsers and Firefox, and size-adjust works in Safari. Support for the other override descriptors in Safari has been more limited, so check current support. Browsers that ignore the overrides simply use the unadjusted fallback, which is no worse than not having one.
Reduce the Number of Font Files
Every weight and style is a separate download unless you use a variable font.
- Audit what you actually use. Designs often call for five weights when two would do. Each one you drop is one fewer request.
- Use a variable font when you need several weights. One variable file often replaces three or more static files and may be smaller than their combined size.
- Consider a system font stack for body text. It loads instantly and looks native on every platform:
body {
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif;
}
A common approach is to use a custom font for headings, where personality matters most, and a system stack for body text.
Cache Fonts Aggressively
Font files rarely change, so serve them with a long cache lifetime and a versioned filename:
Cache-Control: public, max-age=31536000, immutable
When you update a font, change its filename (for example inter-var.v2.woff2) so returning visitors download the new version.
Common Mistakes
- Missing
crossoriginon preload, which causes a duplicate download. - Preloading fonts that aren't used above the fold, which delays more important resources.
- Relying on
auto, which usually means invisible text for up to three seconds on slow connections. - Using
blockfor body text, trading readability for a font users won't miss for a second. - Loading many weights you don't use, often because a font service's embed code included them by default.
Putting It All Together
Here's a solid baseline for a self-hosted font setup:
<head>
<link
rel="preload"
href="/fonts/inter-latin-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="stylesheet" href="/css/main.css" />
</head>
@font-face {
font-family: "Inter";
src: url("/fonts/inter-latin-var.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC, U+2122;
}
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
:root {
--font-sans: "Inter", "Inter Fallback", system-ui, sans-serif;
}
body {
font-family: var(--font-sans);
}
One subsetted variable file, preloaded, with text visible immediately and a metric-matched fallback to keep layout stable during the swap.
Conclusion
Web fonts don't have to cost you speed or stability. font-display decides what happens while a font downloads: swap for fonts that must appear, fallback for a balanced approach on body text, and optional when layout stability and speed matter most. Combine it with a preload for your most important file, WOFF2 and subsetting to shrink downloads, and metric-adjusted fallbacks to minimize layout shift.
Do those few things and your text will appear immediately, look right once the font arrives, and stay put while it happens.


