The Two Problems Web Fonts Create
Web fonts make sites look better. They also introduce two distinct performance problems that confuse developers until someone names them clearly — FOUT and FOIT.
FOUT — Flash of Unstyled Text — happens when the browser renders your text immediately using a fallback system font, then swaps in your web font once it loads. You see the text jump or reflow. FOIT — Flash of Invisible Text — is the opposite: the browser hides all text until the font is downloaded, leaving blank space where words should be. Both are bad. FOIT is arguably worse because users can't read anything, which tanks perceived performance even if the actual load time is fine.
Which behavior you get depends on the browser's default and, crucially, whether you've set the font-display descriptor on your @font-face rules.
The font-display Descriptor
font-display is the main lever you have. Add it to any @font-face block:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
There are five values and they differ in how long the browser waits before showing fallback text:
auto— browser decides. Usually means FOIT for a few seconds, then FOUT.block— 3-second FOIT window, then swap. Rarely what you want.swap— zero-second block. Immediate FOUT, then swap when font loads. Best for body text.fallback— 100ms block, 3-second swap window. If the font takes longer, the fallback sticks. Good compromise.optional— 100ms block, no swap. If the font doesn't load in that window, the fallback is used for the entire page visit. Ideal for non-critical decorative fonts.
For body text, swap is the standard recommendation. For a display font that's purely decorative, optional is worth trying — users on slow connections just see the system font and never experience a jarring reflow.
How Font Loading Actually Works
Fonts aren't render-blocking the way CSS is. The browser discovers a font when it parses a CSS rule that references it and that rule matches an element actually in the document. If no element uses font-family: 'Inter', the font is never requested.
That discovery-at-render-time behavior is why fonts load late. The browser downloads HTML, then CSS, then starts constructing the render tree — and only then figures out which fonts it needs. By that point, the page wants to paint but the font isn't there yet.
The solution is to get the font request started earlier.
Preloading Fonts
The <link rel="preload"> tag tells the browser to fetch a resource early, before the parser would naturally find it:
<link
rel="preload"
href="/fonts/inter-400.woff2"
as="font"
type="font/woff2"
crossorigin
/>
The crossorigin attribute is required even for same-origin fonts. Without it, the browser makes two requests: one from the preload and one from the CSS @font-face rule — the second a different credential mode — so they don't deduplicate and you waste a round-trip.
Preload only the fonts that will definitely be used on first paint. Preloading fonts you don't need on the current page wastes bandwidth and pushes back other critical resources in the queue. Keep it to 1–2 fonts at most, covering your regular weight and maybe your bold.
Self-Hosting vs Google Fonts
Google Fonts is convenient but has two real downsides: a DNS lookup to fonts.googleapis.com plus a connection to fonts.gstatic.com, and privacy implications from third-party requests. If you use the &display=swap parameter, they handle font-display: swap for you, but you still pay the network cost.
Self-hosting eliminates the third-party connection. You serve the font file from your own domain, it gets cached alongside your other assets, and you control the headers. Set a long Cache-Control: max-age=31536000, immutable on your font files — they rarely change.
To download fonts for self-hosting, google-webfonts-helper generates the CSS and files you need. Serve only .woff2 — it has near-universal browser support now and is the most compressed format.
Subsetting: Cutting Font Files Down to Size
A full Inter font file can be 300KB+. Most Latin-script sites only need a small fraction of those glyphs. Subsetting strips out the ones you don't use.
With fonttools you can do this from the command line:
pyftsubset inter.woff2 \
--output-file=inter-subset.woff2 \
--flavor=woff2 \
--layout-features=kern,liga \
--unicodes="U+0020-007E,U+00A0-00FF,U+2018-2019,U+201C-201D"
That range covers basic Latin, common Latin Extended, and the typographic quote characters. The result is often under 30KB. If you're serving a site that only needs English, subsetting is one of the highest-ROI performance moves you can make.
Google Fonts does this automatically for you based on the text= parameter, but when self-hosting you need to do it yourself.
Variable Fonts
Variable fonts pack multiple weights and styles into a single file using a variation axis. Instead of loading inter-400.woff2, inter-500.woff2, and inter-700.woff2 separately, you load one inter-variable.woff2 and control the weight in CSS:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
h1 { font-weight: 720; } /* any value, not just multiples of 100 */
If your design uses more than two or three weights, a variable font is almost certainly smaller than loading individual files. Check support on MDN's font-variation-settings page — it's been widely supported since 2019.
The Fastest Option: System Font Stacks
If performance is the priority and you don't have strong brand requirements, system font stacks are the answer. No request, no FOUT, no FOIT:
body {
font-family:
-apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, sans-serif;
}
This picks the best native font on whatever platform the user is on — San Francisco on macOS/iOS, Segoe UI on Windows, Roboto on Android. The result looks clean, loads instantly, and is what GitHub, Notion, and a lot of fast-loading sites use for body copy. You can still use a custom font for headings while keeping body text as system fonts, splitting the difference.
For developers working with CSS, you can paste the above rules into the CSS Minifier to strip whitespace before shipping. For writing-heavy tools and testing how font choices affect readability, the Word Counter helps you quickly assess text density. And if you're interested in how all of this fits into the broader loading process, the post How Browsers Render a Page covers where font loading sits in the critical rendering path.