loading web fonts without visible flicker
A misconfigured font loading setup either shows invisible text while loading or a visible font swap complete with layout jump the moment the web font arrives. With the right combination of font-display, preload and subsetting, both can be avoided without giving up custom typography.
Table of Contents
- 1. FOIT and FOUT: two symptoms of the same problem
- 2. font-display: the five values in detail
- 3. Preload: bringing the font request forward
- 4. Unicode-range subsetting for smaller files
- 5. Aligning size-adjust and fallback font metrics
- 6. Variable fonts as a loading strategy
- 7. Self hosting vs. Google Fonts and other CDNs
- 8. Measuring: CLS, LCP and the Font Loading API
- 9. Strategies in direct comparison
- 10. Summary
- 11. FAQ
1. FOIT and FOUT: two symptoms of the same problem
In web font loading, two different, undesirable effects have historically shown up. FOIT, Flash of Invisible Text, describes the situation where text stays completely invisible until the web font is fully loaded, because the browser by default waits up to three seconds before falling back to a fallback font. FOUT, Flash of Unstyled Text, describes the opposite: text appears immediately in a fallback font and visibly jumps as soon as the actual web font arrives, often accompanied by a layout shift because different fonts have different character widths.
Both effects are symptoms of the same underlying problem: the browser must decide what to show while an external font file is still loading over the network. A well thought out font loading strategy does not treat this window as an unavoidable evil, but as a designable process, in which it is deliberately decided which font shows when and how the transition to the final web font can be made as unobtrusive as possible. The following sections show the individual building blocks of this strategy in detail.
2. font-display: the five values in detail
The CSS property font-display inside an @font-face rule is the central lever for font loading and defines how the browser treats the period between page load and the web font being fully available. The value block produces classic FOIT: up to three seconds of invisible text, then a fallback, followed by a swap once the web font arrives. The value swap produces classic FOUT: immediate display in the fallback font, practically unlimited wait time for the swap to the web font.
The value fallback is a compromise: a very short blocking phase of about a hundred milliseconds, followed by a short swap window of a few seconds, after which the fallback font is kept permanently even if the web font arrives later. The value optional goes a step further: also a short blocking phase, but no more swap afterward if the web font is not yet available at that point, which is especially useful for returning visitors with a warm cache, since the font is usually already loaded by then. The default value auto leaves the behavior up to the browser, which in practice usually corresponds to block and is therefore rarely the best choice.
/* font-display comparison for the same web font */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-variable.woff2") format("woff2-variations");
font-display: swap; /* immediate fallback text, swap when font arrives */
font-weight: 100 900;
}
/* optional: best for repeat visits, avoids any layout shift on cache hit */
@font-face {
font-family: "Inter Optional";
src: url("/fonts/inter-variable.woff2") format("woff2-variations");
font-display: optional;
font-weight: 100 900;
}
3. Preload: bringing the font request forward
Without additional hints, the browser only discovers a web font file after parsing the CSS and recognizing the corresponding @font-face rule as actually needed, often only after layout has already been computed and it is settled which characters need to be rendered at all. <link rel="preload" as="font" type="font/woff2" crossorigin> in the <head> moves this discovery significantly earlier, letting the browser request the font file in parallel with the CSS instead of waiting for it sequentially.
The effect of preload on font loading is especially noticeable with font-display: swap: the earlier the font file starts loading, the shorter the window in which the fallback font stays visible, and the less often the visible swap even occurs. It matters to set the crossorigin attribute even for fonts on the same domain, because font requests are always made in anonymous CORS mode per specification, a missing crossorigin causes a duplicate request, because the browser cannot match the preloaded request to the actual CSS request.
<head>
<!-- Preload the primary font file so it starts downloading
in parallel with the CSS, not after it -->
<link rel="preload" href="/fonts/inter-variable.woff2"
as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/fonts.css">
</head>
4. Unicode-range subsetting for smaller files
A full web font file often contains several thousand glyphs for languages, Cyrillic and Greek characters, and special symbols that a German or English website never uses. unicode-range inside the @font-face rule tells the browser which character range a particular font file covers, so the browser only downloads the files actually needed for the text that appears on the page.
A typical subsetting for a Western European website separates basic Latin characters from Latin Extended characters such as umlauts and accents, making the file needed for most of the text noticeably smaller than a file with the full character set. For font loading, this directly means shorter loading times, because a smaller file transfers faster over the network, which in turn shortens the window for FOIT or FOUT, regardless of the chosen font-display value.
/* Subsetting: only load the glyph ranges actually used on the page */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153; /* basic Latin */
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-latin-ext.woff2") format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF; /* Latin Extended, e.g. umlauts */
font-display: swap;
}
5. Aligning size-adjust and fallback font metrics
Even with optimal font-display, the risk of a layout shift remains with swap and fallback, because the fallback font and the web font usually have different character widths and line heights. The CSS descriptors size-adjust, ascent-override, descent-override and line-gap-override inside an @font-face rule for the fallback font allow adjusting its metrics to match the web font as closely as possible.
This technique, often called font metric override, defines an additional @font-face rule for an already system available fallback font like Arial, with adjusted metric descriptors, and assigns this adjusted version its own font-family name that sits in the fallback chain ahead of the actual web font. Tools like Google's Font Style Matcher or the npm package fontaine automatically calculate the necessary values by comparing the metrics of both fonts and determining the matching percentage values for size-adjust, reducing the visible jump on font swap to a minimum.
6. Variable fonts as a loading strategy
An often overlooked lever for efficient font loading is using variable fonts instead of multiple static font weights. A classic font family with four styles, regular, bold, italic and bold italic, requires four separate font files and thus four separate network requests. A single variable font file contains all weights and styles in one file, addressable through the CSS property font-variation-settings or the shorthand syntax within the font-weight range.
For font loading, this means: instead of four font requests each with its own FOIT or FOUT window, a single request suffices that makes all needed weights available at once. The total file size of a variable font file is larger than that of a single static file, but usually well below the sum of all needed static styles, especially when a page uses more than two weights at once, which is common in modern typography systems with fine gradations between regular, medium and semibold.
7. Self hosting vs. Google Fonts and other CDNs
Loading fonts through a third party CDN like Google Fonts means the browser must perform an additional DNS lookup and establish a connection to a foreign domain before the actual font file can even be requested. This extra connection setup costs noticeable time, especially on mobile networks with high latency, and extends the window in which FOIT or FOUT is visible, regardless of the chosen font-display strategy.
Self hosting the web font files on your own domain eliminates this extra connection entirely and additionally enables the use of rel="preload", which works less reliably for fonts on foreign domains due to lack of control over cache headers and connection behavior. For projects with strict performance requirements, for example in the context of Core Web Vitals, self hosting is therefore practically always the better choice, even though Google Fonts may seem more convenient due to its own update maintenance.
8. Measuring: CLS, LCP and the Font Loading API
The most important measurable effect of inadequate font loading is Cumulative Layout Shift, visible in Chrome DevTools through the layout shift regions in the rendering tab or programmatically via PerformanceObserver with the layout-shift entry type. A jump of more than 0.1 CLS points right after a font swap strongly indicates poorly adjusted fallback metrics and justifies reviewing the size-adjust configuration from the previous section.
The native document.fonts API, also called the CSS Font Loading API, additionally allows programmatic control over the loading state: document.fonts.ready resolves once all declared fonts have loaded, and document.fonts.check() synchronously checks whether a particular font is already available at a given point in time. This API is well suited for setting a CSS class specifically once the web font is actually loaded, for example to show a deliberately reduced but stable typography while loading, instead of trusting the browser's default behavior.
// Track font loading state to add a class once fonts are ready,
// useful for coordinating fallback-to-webfont transitions manually
document.fonts.ready.then(() => {
document.documentElement.classList.add("fonts-loaded");
});
// Check synchronously whether a specific font is already available
if (document.fonts.check("16px Inter")) {
console.log("Inter is already loaded and cached");
}
// Measure layout shifts potentially caused by font swaps
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log(`Layout shift value: ${entry.value}`);
}
}
}).observe({ type: "layout-shift", buffered: true });
9. Strategies in direct comparison
The following table contrasts the presented font loading strategies to make the choice easier for a concrete project.
| Strategy | Solves | Effort | Recommendation |
|---|---|---|---|
| font-display: swap | Avoiding FOIT | Very low | Almost always sensible as a baseline |
| Preload | Shortening the FOUT window | Low | Always set for the primary font |
| Unicode-range subsetting | Reducing file size | Medium | Important for multilingual character sets |
| size-adjust metric override | Minimizing layout shift | Medium | Recommended for strict CLS targets |
| Variable fonts | Bundling multiple requests | Medium | With more than two font weights |
| Self hosting | Eliminating connection setup | Medium to high | For strict performance budgets |
The practical path for most projects combines several rows of this table: font-display: swap as a baseline, combined with preload for the most important font file, unicode-range subsetting for multilingual content, and a metric adjustment of the fallback font once CLS measurements show a noticeable layout shift on font swap. Self hosting and variable fonts are the more involved but long term worthwhile steps for projects with strict performance goals.
Mironsoft
CSS performance, rendering optimization and modern web frontends
Web fonts without flicker or layout jump?
We configure font-display, preload, unicode-range subsetting and fallback metrics matched to your typography, and measure the effect via CLS and LCP before and after the change.
Font audit
Analysis of existing font requests, subsetting potential and CLS causes
Implementation
Cleanly integrating preload, subsetting and metric override into existing builds
Measurement
Before and after comparison of CLS and LCP with Lighthouse and Core Web Vitals
10. Summary
Successful font loading avoids both FOIT and FOUT by actively shaping the window between page load and web font availability, instead of leaving it to the browser's default behavior. font-display: swap combined with rel="preload" forms the baseline for most projects, unicode-range subsetting considerably reduces the file size to load for multilingual character sets, and metric overrides via size-adjust minimize the layout shift when the fallback font is swapped.
Variable fonts bundle multiple font styles into a single file and thereby reduce the number of parallel font requests, while self hosting eliminates the extra connection setup to third party CDNs. Whoever wants to prove the effect of their font loading strategy measures Cumulative Layout Shift and uses the native document.fonts API to programmatically monitor the loading state and react to it deliberately.
Font loading strategies: the essentials at a glance
FOIT vs. FOUT
Invisible text versus visible font swap, both symptoms of the same loading time problem.
Baseline configuration
font-display: swap plus rel="preload" for the primary web font.
File size
Unicode-range subsetting and variable fonts reduce loading time and request count.
Layout stability
size-adjust metric override minimizes Cumulative Layout Shift on font swap.