Font Loading Optimization: Eliminating Layout Shift and Invisible Text
Web fonts are a common source of layout shift and invisible text. Here's a complete guide to font loading strategies that eliminate both problems without sacrificing design.

James Ross Jr.
Strategic Systems Architect & Enterprise Software Developer
The Two Font Loading Problems That Hurt Performance
Web fonts create two distinct user experience problems that both affect Core Web Vitals scores:
FOIT (Flash of Invisible Text): The browser blocks text rendering until the web font has downloaded. Users see blank spaces where text should be. On a slow connection, critical content like headlines can be invisible for 2-3 seconds.
FOUT (Flash of Unstyled Text): The browser renders text immediately using a fallback system font, then swaps to the web font when it downloads. This causes the text to visibly shift in size and position — a layout shift event that accumulates into your CLS score.
Every web font implementation involves a trade-off between these two problems. The goal of font loading optimization is to make that trade-off as small as possible through a combination of better loading strategies and fallback font tuning.
The font-display Property
font-display is the CSS property that controls what the browser does while a font is loading. Understanding the options is the foundation of font loading optimization:
@font-face {
font-family: 'Geist';
src: url('/fonts/geist.woff2') format('woff2');
font-display: swap;
}
block: Block text rendering for up to 3 seconds, then swap. This is the default behavior — maximum FOIT, no FOUT. Appropriate for critical icon fonts where rendering the wrong symbol is worse than no symbol.
swap: Render immediately with fallback font, swap to web font when available. Zero FOIT, potential FOUT. This is the right default for most body text — users see content immediately, and the font swap is usually brief.
fallback: A short block period (100ms), then fallback font. If the web font loads within 3 seconds, swap. After 3 seconds, use the fallback font permanently for that page load. A reasonable middle ground.
optional: Very short block period, then fallback. The browser may or may not load the web font depending on network conditions. Good for non-essential decorative fonts.
For most applications, font-display: swap is the correct choice for body and heading fonts. The FOUT is a CLS cost, but it's addressable through fallback font metric matching (covered below). FOIT is not addressable and causes worse user experience.
Preloading Critical Fonts
By default, the browser discovers font files by parsing the CSS, finding the @font-face rules, then encountering elements that use those fonts before initiating the download. This is late in the resource loading process.
Font preloading moves the download earlier, reducing both FOIT duration and FOUT duration:
<link
rel="preload"
href="/fonts/geist-regular.woff2"
as="font"
type="font/woff2"
crossorigin
>
The crossorigin attribute is required even for same-origin fonts — the font fetch spec requires it.
What to preload: Preload only the fonts that render visible text above the fold. Preloading too many fonts creates bandwidth contention and slows the initial render of more critical resources. The practical limit is usually 2-3 preloads per page.
Preload only the fonts in use: If your heading uses a different weight than your body text, preload both weights. Don't preload weights that don't appear in the initial viewport.
Self-Hosting vs Google Fonts
Google Fonts is convenient but slower than self-hosting for several reasons: an additional DNS lookup for fonts.googleapis.com and fonts.gstatic.com, no cache benefits on the font files themselves (CDN cache is shared but browser cache is per-origin), and you can't control the loading strategy (no direct preload without fetching the CSS first).
Self-hosting eliminates all three issues. With self-hosting, you can:
- Add
preloadtags directly for specific font files - Serve fonts from your own CDN domain (no extra DNS lookup)
- Control compression and caching headers
- Subset fonts to only the characters you need
google-webfonts-helper (gwfh.mranftl.com) makes it easy to download Google Fonts for self-hosting with the correct CSS and WOFF2 files.
Font subsetting: If you're using Latin characters only, you don't need the full font file with Cyrillic, Greek, and Vietnamese character ranges. Subsetting removes unused character ranges. pyftsubset (fonttools) or Glyphhanger can subset fonts programmatically. Typical reduction: 50-70% smaller files.
Fallback Font Metric Matching: Eliminating CLS from FOUT
This is the most sophisticated font optimization technique and the one that most developers skip — to the detriment of their CLS scores.
When a browser swaps from a fallback font to a web font, the text reflows if the two fonts have different metrics. The text takes up more or less space, changes height, and everything shifts. The fix is to make the fallback font's metrics match the web font's metrics as closely as possible.
CSS provides four properties for fallback font metric override:
@font-face {
font-family: 'Geist-fallback';
src: local('Arial');
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 104%;
}
body {
font-family: 'Geist', 'Geist-fallback', sans-serif;
}
The values tell the browser to render Arial with the same metrics as Geist. When Geist loads and swaps in, the text occupies the same space — no layout shift.
Finding the right values: Use the Fontaine library (for Nuxt/Next.js projects) which calculates these values automatically from the font files, or use the fontpie tool. Manually tweaking these values is feasible but tedious.
Variable Fonts
Variable fonts are a single font file that contains a continuous range of weights, widths, and other axes rather than separate files for each variant. A variable font file is typically smaller than multiple static font files for the same family when you use more than two or three weights.
If your design uses three or more font weights (light, regular, bold — common for headings plus body), a variable font is almost certainly smaller than the equivalent set of static fonts.
@font-face {
font-family: 'Geist';
src: url('/fonts/geist-variable.woff2') format('woff2-variations');
font-weight: 100 900; /* weight range supported */
font-display: swap;
}
With a variable font, you can use any weight between 100 and 900 without a separate file.
Framework-Specific Implementations
Next.js: The next/font package handles font self-hosting, subsetting, and fallback metric generation automatically at build time. It generates the size-adjust and override values. This is the easiest way to get optimal font loading in a Next.js project.
Nuxt.js: The @nuxtjs/fontaine module provides similar functionality — automatic fallback metric calculation and @font-face injection. The nuxt-fonts module adds Google Fonts self-hosting.
Plain HTML/CSS: Self-host the fonts, add preload tags for above-the-fold fonts, use font-display: swap, and manually set fallback font metric overrides.
Measuring Font Loading Impact
Tools to verify your font loading is working correctly:
- WebPageTest with "Filmstrip" view — shows exactly when the font swap occurs visually
- Lighthouse — flags FOIT and CLS from font swaps
- Chrome DevTools Performance tab — shows font network requests and swap timing
- CrUX data in PageSpeed Insights — real user CLS from font swaps will show in field data
The target: zero CLS attributable to font loading, zero FOIT, and web fonts loaded within 1 second of page start on a good connection.
Font loading is one of those areas where small implementation details make a significant difference to both visual stability and perceived performance. If you're seeing CLS or layout issues on your site related to fonts, book a call at calendly.com/jamesrossjr and let's diagnose and fix it.