Font Loading Strategies
The Problem
Web fonts are a major source of layout shift (CLS) and poor perceived performance. When a browser downloads a custom font, it must decide what to show while waiting — invisible text (FOIT: Flash of Invisible Text) or fallback-styled text (FOUT: Flash of Unstyled Text). AI agents typically add a @font-face declaration or a Google Fonts <link> tag and consider the job done, ignoring the loading behavior entirely. This produces visible layout shifts when fonts load, blank text during the loading period, or unnecessary network requests for fonts that could be deferred.
The Solution
A robust font loading strategy combines several techniques: the font-display descriptor to control rendering behavior, <link rel="preload"> for critical fonts, system font stacks as fallbacks, and metric overrides to minimize layout shift.
Code Examples
font-display Values
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
/* swap: Show fallback immediately, swap when font loads.
Best for body text where content must be readable. */
font-display: swap;
}
@font-face {
font-family: "HeadingFont";
src: url("/fonts/heading.woff2") format("woff2");
/* optional: Use the font only if it's already cached.
Best for non-critical text where layout stability matters more. */
font-display: optional;
}
Summary of font-display values
| Value | Block period | Swap period | Best for |
|---|---|---|---|
auto | Browser default | Browser default | Rarely the right choice |
block | Short (3s) | Infinite | Icon fonts only |
swap | Extremely short | Infinite | Body text, content fonts |
fallback | Very short (100ms) | Short (3s) | Balancing FOUT and CLS |
optional | None | None | Non-critical fonts, max CLS control |
Preloading Critical Fonts
<head>
<!-- Preload only the most critical font file (usually body text, regular weight) -->
<link
rel="preload"
href="/fonts/body-regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<!-- Do NOT preload every weight/style — only the one(s) needed for above-the-fold content -->
</head>
The crossorigin attribute is required even for same-origin fonts — without it, the font will be fetched twice.
System Font Stack as Fallback
:root {
--font-system: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
"Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
"DejaVu Sans Mono", monospace;
}
body {
font-family: "MyFont", var(--font-system);
}
code {
font-family: var(--font-mono);
}
Reducing Layout Shift with Metric Overrides
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
}
/* Adjust the fallback font metrics to match the web font */
@font-face {
font-family: "MyFont Fallback";
src: local("Arial");
size-adjust: 104.7%;
ascent-override: 93%;
descent-override: 25%;
line-gap-override: 0%;
}
body {
font-family: "MyFont", "MyFont Fallback", sans-serif;
}
Complete Strategy: Optimal Performance
<head>
<!-- 1. Preload the critical font -->
<link
rel="preload"
href="/fonts/body-regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<!-- 2. Inline critical @font-face rules -->
<style>
@font-face {
font-family: "Body";
src: url("/fonts/body-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Body";
src: url("/fonts/body-bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Heading";
src: url("/fonts/heading.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: optional;
}
</style>
</head>
Font Subsetting
/* Latin subset only — significantly reduces file size */
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont-latin.woff2") format("woff2");
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Common AI Mistakes
- Not specifying
font-displayat all, leaving the browser to use its default behavior (typicallyauto, which causes FOIT in most browsers) - Preloading every font weight and style, which congests the network and can actually slow down the page
- Missing the
crossoriginattribute on<link rel="preload">, causing the font to be downloaded twice - Using only
woffinstead ofwoff2— the latter provides 15-30% better compression and is supported by all modern browsers - Loading Google Fonts via
<link>withoutdisplay=swapparameter (e.g.,fonts.googleapis.com/css2?family=Roboto&display=swap) - Not providing a system font fallback stack, leaving
sans-serifas the only fallback - Including fonts for weights that are never used in the design (e.g., loading 6 weights when only regular and bold are used)
- Using
font-display: blockfor body text, causing invisible text for up to 3 seconds on slow connections
When to Use
font-display: swap
- Body text and primary reading content
- Any text that must be immediately readable
font-display: optional
- Heading or display fonts where layout stability is critical
- Fonts used for decorative purposes
- Return visits where the font is likely cached
Preloading
- The single most critical font file (usually body regular weight)
- Above-the-fold heading fonts on landing pages
- Never more than 1-2 font files
System font stacks
- When performance is the top priority
- Internal tools and admin interfaces
- Fallback chains for custom web fonts