Typography is an essential part of a site's identity, and web fonts let you use typefaces other than those installed on the device. They come at a cost, however: they are files to download, often discovered late by the browser, and until they arrive the text may stay invisible or suddenly change appearance. With a few precautions you can have well-crafted type without penalising the speed and stability of the page.
The problem: invisible text or jumping text
When a CSS rule requires a font that has not yet been downloaded, the browser has two options:
- FOIT (Flash of Invisible Text): it hides the text until the font arrives. On a slow connection, users see a page with blank spaces where the headings should be.
- FOUT (Flash of Unstyled Text): it shows the text immediately in a fallback font and swaps it when the web font is ready. Content is readable straight away, but the swap can change the text size and shift the layout.
In general the second behaviour is preferable: reading right away matters more than how the first moments look. It is controlled with font-display.
font-display
The property is declared in the @font-face rule:
@font-face {
font-family: "Body";
src: url("/fonts/body-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
The main values:
| Value | Behaviour |
|---|---|
swap |
shows the fallback font immediately and swaps it when the web font arrives |
optional |
very short wait; if the font is not ready, uses the fallback for the whole visit |
fallback |
short wait, then fallback; swaps only if the font arrives within a short time |
block |
hides the text for a longer period before using the fallback |
swap is the most common choice for body text. optional is interesting when visual stability matters a lot: on the first visit users may see the system font, but the web font is cached and used on later visits, with no shift at all.
A single format: WOFF2
There is no longer any need to list five different formats in src. WOFF2 offers the best compression and is supported by all current browsers. If you receive a font in TTF or OTF format, convert it to WOFF2 before publishing it, checking that the font licence allows web use.
Hosting fonts on your own domain
Loading fonts from an external service is convenient, but it means an additional connection to another domain and, in some cases, sending the visitor's IP address to third parties, with possible implications for user privacy. Hosting the WOFF2 files alongside the rest of the site:
- removes the additional connection;
- gives you full control over caching and versions;
- simplifies the Content Security Policy;
- avoids passing visitor data to external providers.
With file names that contain a hash you can give fonts a very long cache lifetime.
Preloading critical fonts
The browser discovers a font only after downloading the CSS and finding an element that uses it: often that is late. For the main body font, a preload brings the download forward:
<link rel="preload" href="/fonts/body-regular.woff2"
as="font" type="font/woff2" crossorigin>
The crossorigin attribute is required even for fonts on the same domain, otherwise the file is downloaded twice. Preload only one or two truly essential files: preloading every variant (italic, bold, semibold) takes bandwidth away from more important resources.
Reducing weight: subsets and unicode-range
A full font can contain thousands of glyphs for alphabets your site will never use. Subsetting creates a version containing only the characters you need, for example the Latin alphabet with accents and punctuation. The savings can be considerable.
With unicode-range you can split the font into several files and let the browser download only those containing the characters actually present on the page:
@font-face {
font-family: "Body";
src: url("/fonts/body-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2018-201F;
font-display: swap;
}
Make sure the subset includes every character you need: accented letters, typographic quotes, the euro sign, long dashes. A missing character is shown in the fallback font, creating an inconsistent effect.
Variable fonts
A variable font contains a whole range of weights (and sometimes widths or slants) in a single file. If you use three or four weights of the same typeface, a variable file is often lighter than the sum of the static files and also offers intermediate weights:
@font-face {
font-family: "Body";
src: url("/fonts/body-variable.woff2") format("woff2");
font-weight: 300 800;
font-display: swap;
}
h1 { font-weight: 720; }
Tuning the fallback font
When the web font replaces the fallback, the difference in letter width and height moves the text and contributes to CLS. The size-adjust, ascent-override, descent-override and line-gap-override descriptors let you adapt a system font to the web font's metrics:
@font-face {
font-family: "Body Fallback";
src: local("Arial");
size-adjust: 105%;
ascent-override: 92%;
}
body {
font-family: "Body", "Body Fallback", sans-serif;
}
Consider system fonts
For interfaces, admin panels and long texts, a system font stack (system-ui, -apple-system, "Segoe UI", Roboto, sans-serif) offers highly legible typefaces that are already on the device and cost nothing. You can reserve the web font for headings, where visual identity matters most.
In short
- Use
font-display: swaporoptionalso text is not hidden while loading. - Publish only WOFF2 and, where possible, host fonts on your own domain.
- Preload only the main body font, with the
crossoriginattribute. - Cut weight with subsets,
unicode-rangeand variable fonts when you use several weights. - Tune the fallback font with
size-adjustto limit layout shifts.