Introduction
Web fonts enhance design but come at a cost. While a custom font loads, browsers must decide: show invisible text (FOIT — Flash of Invisible Text) or show a fallback font (FOUT — Flash of Unstyled Text). Either choice impacts CLS (Cumulative Layout Shift) and user experience. This article covers strategies to load fonts reliably while minimizing layout shifts.
The font-display Descriptor
The font-display property in @font-face controls how a font is displayed during loading:
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap;
}
| Value | Behavior | Use Case |
|---|---|---|
auto | Browser default (usually FOIT) | Default |
block | Invisible text up to ~3s, then swap | Short block period |
swap | Fallback font immediately, swap when ready | Maximum readability |
fallback | Invisible text ~100ms, fallback ~3s, then swap | Balance |
optional | Short block, optional download | Slow connections |
swap is the most common choice — it shows fallback text immediately, then swaps when the custom font loads, completely eliminating FOIT.
Preloading Critical Fonts
Use <link rel="preload"> in the <head> to start font downloads early:
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
The crossorigin attribute is required for fonts loaded from a different origin, even same-origin fonts served via CDN. Without it, the preload is ignored.
Self-Hosting vs. CDN
| Approach | Pros | Cons |
|---|---|---|
| Self-host | Full control, no extra DNS lookup, cache control | Bandwidth cost |
| Google Fonts / CDN | Fast edge delivery, automatic subsetting | Extra DNS + TLS handshake |
For critical fonts, self-hosting often performs better because it eliminates the extra connection to the font provider. Use preconnect if you must use a third-party font service:
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
Font Subsetting
Including every glyph in a font file wastes bandwidth. Subsetting removes unused characters:
# Using pyftsubset (fonttools)
pyftsubset Roboto-Regular.ttf --unicodes="U+0020-007E,U+00A0-00FF"
Most CDN font services do this automatically. For self-hosted fonts, generate subsets for each language you support. Tools like glyphhanger or fonttools can output files as small as 10-20 KB instead of 100+ KB.
WOFF2: The Modern Standard
WOFF2 (Web Open Font Format 2) offers 30-50% better compression than WOFF:
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2'),
url('/fonts/myfont.woff') format('woff'); /* fallback */
}
All modern browsers support WOFF2. Use it as the primary format with WOFF as a fallback.
Preventing Layout Shift with size-adjust
The size-adjust descriptor in @font-face lets you normalize metrics so the fallback and custom fonts occupy the same space:
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 95%;
ascent-override: 85%;
descent-override: 20%;
}
Use tools like the size-adjust calculator (Chrome DevTools > Rendering > Font Size Adjust) to find the correct values. This dramatically reduces CLS when the font swaps.
Complete Optimization Recipe
<head>
<!-- Preconnect to font origin -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<!-- Preload the main font -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
</head>
@font-face {
font-family: 'Main';
src: url('/fonts/main.woff2') format('woff2');
font-display: swap;
}
body {
font-family: 'Main', 'Fallback', sans-serif;
}
Conclusion
Optimizing web fonts requires a multi-layered approach: use font-display: swap to prevent FOIT, preload critical fonts, subset your files, serve WOFF2, and apply size-adjust to minimize CLS. These techniques together ensure fast, stable, and beautiful typography.
