Introduction
Web fonts make your site look great but can seriously hurt performance if loaded incorrectly. Flash of Invisible Text (FOIT) makes content unreadable while fonts download. Flash of Unstyled Text (FOUT) causes jarring layout shifts.
The key challenge is balancing visual design with performance. Modern CSS and browser APIs give us excellent tools to achieve both fast loading and beautiful typography.
This guide covers every technique from basic font-display to advanced strategies like subsetting and variable fonts.
Key Concepts
font-display Property
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when loaded */
}
Preloading Critical Fonts
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
<!-- crossorigin is required even for same-origin fonts! -->
Font Subsetting
pyftsubset Inter-Regular.ttf \
--output-file=Inter-Regular-Latin.woff2 \
--flavor=woff2 \
--unicodes='U+0000-00FF,U+0131,U+0152-0153'
Practical Examples
1. Optimal Google Fonts Loading
<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;600;700&display=swap" rel="stylesheet">
2. Variable Fonts
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
3. Fallback Font Matching
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
ascent-override: 90%;
descent-override: 22%;
size-adjust: 107%;
}
Best Practices
- ✅ Use font-display: swap for body text, optional for non-critical fonts
- ✅ Preload critical fonts (1-2 max) used above the fold
- ✅ Self-host fonts for better performance and privacy
- ✅ Use variable fonts to reduce total font file downloads
- ✅ Subset fonts to include only needed character sets
- ❌ Don't preload more than 2-3 font files
- ❌ Don't use font-display: block — it causes FOIT
Common Pitfalls
- 🚫 Forgetting crossorigin on preload — font preloads without it are fetched twice
- 🚫 Loading too many font weights — use variable fonts or limit to 2-3
- 🚫 Not matching fallback metrics — causes CLS when custom font swaps in
- 🚫 Using Google Fonts without preconnect — adds 100-300ms