Font Loading
10 minFont Loading
Loading webfonts well means the right weights and styles arrive in the right format with the right CSS, so the browser never has to fake what the design needs. Get this wrong and two things happen: the browser produces faux bold or faux italic by mathematically distorting the regular weight, and users stare at invisible or shifting text while files download.
This lesson covers both sides: the typographic setup (which files, which declarations) and the performance pipeline (preconnect, preload, font-display).
Declare every style you use
For body text you typically need four files: regular, italic, bold, bold-italic. Each is a separate @font-face declaration mapping the same font-family name to a different font-weight and font-style:
@font-face {
font-family: 'Source Serif';
src: url('/fonts/source-serif-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Source Serif';
src: url('/fonts/source-serif-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Source Serif';
src: url('/fonts/source-serif-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Source Serif';
src: url('/fonts/source-serif-bold-italic.woff2') format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
}
When the browser encounters <em> or <strong>, it walks this table for a real match. If the table is incomplete, the browser synthesises — and that's where faux styles come from. Faux bold thickens every stroke uniformly; faux italic slants the regular at 12 degrees. Both look wrong, especially next to real variants.
Use WOFF2
WOFF2 is the modern web font format: smaller than WOFF and TTF, supported by every current browser. Don't ship WOFF, EOT, or SVG fallbacks unless your audience meaningfully includes IE11. A single format('woff2') source is sufficient.
Variable fonts
A variable font packages multiple weights — and sometimes widths, italic, optical size — into a single file with smooth interpolation between them. Often smaller than two or three static files combined, and it lets the design call any weight value (font-weight: 437) rather than just predefined steps.
The trade-off: the single file is larger than any individual static file. If you only need regular and bold, two static WOFF2 files may be smaller than one variable file. If you need four or more weights, the variable file usually wins.
The performance pipeline
1. Preconnect to the font origin
If fonts are served from a different origin (Google Fonts, your CDN), give the browser a head start on the TLS handshake:
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
Without this, the font request waits for DNS lookup, TCP handshake, and TLS handshake to complete sequentially. With it, those steps run in parallel with HTML parsing.
2. Preload critical fonts
For the one or two fonts that appear above the fold, preload so the browser starts downloading as soon as it parses the head:
<link
rel="preload"
href="/fonts/inter-regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Preload only fonts that are definitely used above the fold. Preloading every weight wastes bandwidth and delays the critical path.
The crossorigin attribute is required even for same-origin fonts — fonts are always fetched in CORS mode. Omitting it causes a double-fetch: one from the preload (non-CORS) and one from the @font-face rule (CORS), wasting the preload entirely.
3. Choose a font-display strategy
font-display tells the browser what to show while the font downloads.
| Value | Behavior | Use case |
|---|---|---|
| auto | Browser decides — usually 3s invisible text (FOIT) | Never use explicitly |
| swap | Show fallback immediately, swap when loaded (FOUT) | Default for most fonts |
| fallback | Short block (~100ms), short swap window (~3s), then permanent fallback | Non-critical fonts |
| optional | Brief block; use cached font or permanently fall back | Slow networks, progressive enhancement |
swap is the right default for most projects. The user sees something immediately — even if it's the fallback — rather than staring at invisible text.
Taming the flash (FOUT)
font-display: swap trades invisible text (FOIT) for a visible flash when the webfont swaps in (FOUT). The closer the fallback font's metrics match the webfont, the less jarring the swap. Use size-adjust, ascent-override, and descent-override on the fallback to close the gap:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
Tools like Fontaine and next/font automate this calculation. The goal is a swap so subtle that users don't notice it — the text barely shifts because the fallback already occupies the same space.
Common mistakes
No font-display at all. Defaults to auto, which on most browsers means three seconds of invisible text on slow networks. Always declare it.
Preloading every font weight. Loads a megabyte before the page paints. Preload only the one or two weights used above the fold; let the rest load normally.
Missing crossorigin on preload. The preloaded resource doesn't match the @font-face fetch. Browser downloads the font twice and the preload was wasted.
Loading weights you don't use. If the design only uses 400 and 600, don't load 300, 500, 700, 800, 900. Every unused weight is wasted bytes and a slower page.
Relying on Google Fonts without preconnect. Two origins are involved: fonts.googleapis.com (CSS) and fonts.gstatic.com (font files). Preconnect to both, or self-host the fonts to eliminate the extra origins entirely.