Web fonts are responsible for two categories of performance problems: layout shifts (CLS) from font swapping, and load time overhead from large font files or external connections. This guide covers both, with the specific settings that achieve a CLS of 0 from fonts and cut font load time by 60%.
WOFF2: the only format you need
WOFF2 uses Brotli compression and has been supported by all major browsers since 2016. There is no reason to serve WOFF, TTF, or EOT anymore. Serving multiple formats adds complexity and bandwidth negotiation overhead. Serve WOFF2 only.
Compression comparison for a typical Latin font:
| Format | File size |
|---|---|
| TTF | 220 KB |
| WOFF | 140 KB |
| WOFF2 | 95 KB |
| WOFF2 subsetted (Latin only) | 18 KB |
Subsetting: the biggest win
Most fonts include glyphs for hundreds of Unicode ranges. If your site is English-only, you only need the Latin character set — roughly 250 glyphs. Subsetting to Latin reduces a typical font from 80–150 KB to 15–25 KB.
Tools for subsetting:
- pyftsubset (fonttools): command-line, precise control.
pyftsubset font.ttf --unicodes='U+0020-007F' --flavor=woff2 - Google Fonts with subset parameter:
&subset=latinon the URL. - next/font: Next.js automatically subsets and self-hosts Google Fonts at build time. Zero external connection.
Self-hosting vs Google Fonts
Google Fonts requires:
- DNS resolution for
fonts.googleapis.com - TCP + TLS to
fonts.googleapis.com - CSS fetch to get the
@font-facerules - DNS resolution for
fonts.gstatic.com - TCP + TLS to
fonts.gstatic.com - Font file fetch
Self-hosting (or using next/font) removes steps 1–5. On a fast connection the difference is ~100 ms; on a slow connection it can be 500+ ms.
font-display: controlling the swap
The font-display descriptor controls what happens while the font is loading:
| Value | Behavior | CLS risk |
|---|---|---|
| block | Invisible text for up to 3 s | Low (no layout shift, but FOIT) |
| swap | Fallback immediately, swaps when ready | High (layout shift on swap) |
| fallback | Very brief block, then swap | Medium |
| optional | Very brief block, no swap if not ready | Zero (never swaps) |
For body text: font-display: optional eliminates CLS entirely but means some users see the fallback font. For headings: font-display: swap is acceptable since a heading shift is less visually jarring than body text shift.
Preloading fonts
Add a preload link for your most critical font file in <head>:
<link rel="preload" as="font" type="font/woff2" href="/fonts/inter.woff2" crossorigin="anonymous">
The crossorigin attribute is required even for same-origin fonts, because browsers fetch fonts in anonymous CORS mode. Missing it causes a double download.
Variable fonts
A variable font contains all weights and styles in a single file. A traditional font family (regular + bold + italic + bold-italic) ships 4 files. A variable font ships 1 file — usually smaller than any single weight:
| Approach | Files | Total size |
|---|---|---|
| Static fonts (4 weights) | 4 | 80 KB each = 320 KB |
| Variable font | 1 | ~70 KB |
Use variable fonts when you need more than 2 weights of the same family. The font must support variable format — most modern Google Fonts do.