Every modern site uses custom web fonts: Google Fonts, Adobe Fonts, self-hosted files. Load them wrong and you tank LCP and jar the user.
I’ve optimised fonts on three projects. Each had a different answer to the FOUT-versus-FOIT trade-off.
What FOUT and FOIT mean
FOUT (Flash of Unstyled Text): while the custom font loads, the browser renders with a fallback. When the custom font arrives, the text swaps. The user stumbles on “the font changed”, but they can read.
FOIT (Flash of Invisible Text): the text stays invisible until the custom font is ready. Blank space. The user thinks “where’s the text?”. Then everything pops in at once.
Which is worse? Both are bad, but FOIT is worse because the content is blocked.
font-display: CSS’s answer
The font-display property controls the preference inside a font-face declaration:
@font-face{
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}The values:
- auto: browser chooses (usually FOIT)
- block: brief invisible period, then fallback, then swap when the custom font arrives
- swap: show fallback immediately, swap when the custom font arrives. Classic FOUT
- fallback: very brief block, then fallback, and if the custom font loads too late you stay on fallback
- optional: block, then fallback. The custom font goes into cache for future requests but isn’t used on this one
Which value to pick
I go with swap almost always. Exceptions:
Brand-critical logo fonts: block is fine. The logo looks wrong without the custom font, so a brief blank is better.
Long-form content (blog, article): swap, no question. The text has to be visible.
Navigation, UI: swap or optional. If your fallback has similar metrics, even FOUT is barely noticeable.
Brand fonts with no close fallback: consider fallback. If the custom font is too slow, the fallback sticks around and you avoid layout shift.
Font loading strategy
font-display alone isn’t enough. How fast the font loads matters just as much.
1. Use WOFF2. Modern format. 30% smaller than WOFF. Browsers without WOFF2 support are a rounding error.
2. Subset. A Turkish site doesn’t need Arabic glyphs. Google Fonts defaults to the latin subset with latin-ext for Turkish diacritics. For self-hosted fonts, pyftsubset or an online tool does the job.
3. Preload critical fonts. In <head>:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>The browser learns about the font early, kicks off the fetch without waiting for CSS parsing, and the FOIT/FOUT window shrinks.
4. Self-host instead of Google Fonts?
Google Fonts used to be faster because of CDN sharing. Chrome now partitions its font cache, so self-hosting is at worst equal, often faster.
Self-host wins:
– Same origin (no extra DNS lookup)
– Full control (subset, format)
– GDPR (no IP leaks to Google)
Google Fonts wins:
– Easier setup
– Big font catalogue
– Version management handled for you
If GDPR matters, self-host. In most cases I recommend self-hosting regardless.
Variable fonts
A variable font packs many weights and styles into a single file. Instead of separate light, regular, and bold files for Inter, you get one Inter-Variable.woff2.
Advantages:
– One HTTP request
– Total size smaller than the combined separate files
– Intermediate weights are available (like weight 375)
Control the weight with font-variation-settings:
.heading {
font-family: 'Inter';
font-weight: 650; /* variable weight */
}Every modern browser supports this. Inter, Roboto Flex, Source Sans 3, and plenty of others ship as variables.
Designing the fallback stack
If you use swap, the fallback matters:
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}The fallback should have similar metrics to the custom font, or the swap causes a big layout shift and wrecks CLS.
size-adjust: the advanced technique
CSS Font Module Level 4 introduced size-adjust, ascent-override, descent-override, and line-gap-override to match a fallback font to the custom font’s metrics.
@font-face{ font-display:swap;
font-family: 'FallbackInter';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
}
body {
font-family: 'Inter', 'FallbackInter', sans-serif;
}The browser is actually rendering Arial, but the metrics match Inter. When the swap happens, the shift is invisible.
This gets CLS close to zero. Tools like fontpie calculate the right values automatically.
Measuring the impact
- FCP: fast if the font isn’t blocking
- LCP: delayed if hero text waits on the custom font
- CLS: how much the layout shifts during the swap
- Chrome DevTools Performance panel: font request timing
If Lighthouse warns about “Ensure text remains visible during webfont load”, you’re missing font-display: swap.
Icon fonts versus SVG
Font Awesome and friends are the old way. By 2026, SVG icons are the stronger choice:
- Icon font: you download the full icon set (500+ KB) to render one icon
- Inline SVG: only the icon you actually use
- SVG sprite: a separate file, cacheable
Drop icon fonts. Switch to SVG.
An e-commerce example
A client used a custom display font for big hero headings on product pages. They had FOIT, and LCP was 3.2 seconds.
Optimisation:
– Added font-display: swap
– Switched to a WOFF2 variable font (800 KB to 210 KB)
– Added <link rel="preload"> for the critical font
– Tuned the fallback metrics with size-adjust
Result:
– LCP 3.2s to 1.6s
– CLS negligible (size-adjust made the shift invisible)
– Visual polish preserved
Common mistakes
1. Too many font families. Three or more families means six or more requests. One or two is enough.
2. Loading five or more weights. Regular plus bold is usually enough. Light, medium, semibold, black are rarely needed.
3. Auto-loading italics. If you don’t use italics, don’t load them.
4. Third-party font scripts. Adobe Fonts’ JS loader and similar tools add extra JS. Self-hosting is cleaner.
5. @import inside CSS. The font doesn’t start loading until the CSS does. <link rel="preload"> is the fix.
Closing thought
Font optimisation is a quick win. One or two hours of work can shave seconds off LCP. My approach:
- Prefer WOFF2 variable fonts
- font-display: swap
- Preload critical fonts
- Matching fallback metrics
- Subset (latin-ext for Turkish sites)
- Two or three weights is enough
These six steps get you 80% of the benefit. Two or three hours pays off across years of improved LCP.