Table of Contents
1. Why Fonts Are a Performance Killer
Open any modern website, and every character you see may be backed by one or more font files. A complete Chinese font easily weighs 5–10 MB, and even for Latin fonts, a single weight’s WOFF2 file typically runs between 50–200 KB. If a page references 3 weights plus italics, fonts alone can easily consume over 500 KB of downloads—more than the entire page’s HTML, CSS, and JS combined.
What’s worse, fonts are “blocking resources”: the browser only downloads them after determining which fonts are needed, and text rendering depends on those fonts. This leads to two classic phenomena on web pages:
- FOUT (Flash of Unstyled Text): Text is first rendered with a system font, then “jumps” to the target font once the download completes;
- FOIT (Flash of Invisible Text): Text is completely invisible (blank) until the font is ready.
These two are essentially two sides of the same problem: font downloads have latency, and rendering can’t wait indefinitely. The goal of this article is to make that “wait” as small and as graceful as possible.
2. font-display: The Core Switch for Controlling Wait Behavior
CSS’s font-display is the most direct tool for controlling rendering behavior during font loading. It’s defined in @font-face and has five values, with semantics built around the “block period” and “swap period”:
@font-face { font-family: 'MyFont'; src: url('/fonts/myfont.woff2') format('woff2'); font-display: swap;}| Value | Block Period | Swap Period | Typical Result |
|---|---|---|---|
block | Up to 3s | Infinite | Text invisible for up to 3 seconds (FOIT), then displayed |
swap | Extremely short (~0ms) | Infinite | System font shown immediately, replaced after loading (FOUT) |
fallback | ~100ms | ~3s | Very brief FOIT; if not loaded within 3 seconds, system font is used permanently |
optional | ~100ms | None | Very brief FOIT; if loading doesn’t make it in time, no font switch occurs |
auto | Browser decides | Browser decides | Equivalent to block’s behavior |
How to choose? A widely accepted practice is:
- Body text uses
swap: Users can read content immediately, and the font “jump” is within acceptable bounds; - Brand logos and icon fonts use
optionalorblock: For these elements, it’s better to show nothing than to have the jarring effect of “using another font first, then switching”; - Almost never use the default
blockfor long body text—3 seconds of invisible text will drive users away immediately.
3. preload: Giving Font Downloads a Head Start
font-display solves “what to display during loading,” but the ideal strategy is to start font downloads earlier. By default, the browser must parse the CSS, build the render tree, and discover that some text actually uses the font before initiating a font request—this “discovery” often happens in the mid-to-late stages of page rendering.
Using <link rel="preload">, you can tell the browser right in the HTML head: “This font is important, go download it now”:
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>Two key points:
as="font"is required—it lets the browser know this is a font resource and assigns the correct priority;- You must add
crossorigin—even if the font is on the same domain as you. This is a classic pitfall: font requests have “cross-origin” semantics (CORS) by default, and omittingcrossorigincauses the font to be downloaded twice: once for the preload request and once for the actual usage request.
If your font comes from a third-party CDN (like Google Fonts), you can also pair it with preconnect to establish the connection early, saving DNS resolution and TCP/TLS handshake time:
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>4. Self-Hosting vs Third-Party CDN
Google Fonts reduces the cost of “using beautiful fonts” to a single line of <link>, but self-hosting (placing font files on your own server/CDN) is re-emerging as the mainstream choice, for three reasons:
- Performance: Eliminates cross-origin connections, third-party redirects (Google Fonts’ CSS 302-redirects to gstatic), and extra TLS handshakes;
- Privacy compliance: Multiple EU data protection authorities have determined that Google Fonts transmits user IPs to Google, and direct embedding violates GDPR (a German court has ruled on this);
- Control: Version, caching strategy, and loading strategy are entirely in your hands.
Self-hosting isn’t complicated—after getting the WOFF2 files from the Google Fonts download page, just write a standard @font-face. Modern browsers universally support WOFF2, which is on average 30% smaller than WOFF and TTF, so you can keep only WOFF2:
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; src: url('/fonts/inter-regular.woff2') format('woff2'); font-display: swap;}5. Subsetting and unicode-range: Load Only the Characters You Use
For Chinese-language sites, this step delivers the biggest gains. A complete Chinese font starts at several MB, but a page may actually use only a few hundred Chinese characters. Subsetting means slicing the font down by character set, then using unicode-range to let the browser download on demand:
@font-face { font-family: 'NotoSansSC'; src: url('/fonts/noto-sans-sc-latin.woff2') format('woff2'); unicode-range: U+0000-00FF; /* Latin characters */ font-display: swap;}
@font-face { font-family: 'NotoSansSC'; src: url('/fonts/noto-sans-sc-common.woff2') format('woff2'); unicode-range: U+4E00-9FFF; /* Common Chinese characters */ font-display: swap;}unicode-range tells the browser to download only the subset whose character range is actually matched by the page. If a piece of text contains only English letters, the Chinese subset won’t be requested at all. Tools (such as subset-font, fonttools’s pyftsubset) can automate the splitting.
6. Variable Fonts: One File Replaces an Entire Font Family
Variable Fonts carry continuous axes in a single file—weight, width, slant, optical size, etc. What previously required 4 files (“Regular + Medium + Bold + Italic”) is now handled by 1 file, reducing requests from 4 to 1:
@font-face { font-family: 'InterVariable'; src: url('/fonts/inter-var.woff2') format('woff2-variations'); font-weight: 100 900; /* Declare variable range */ font-display: swap;}
.text { font-family: 'InterVariable', sans-serif; font-weight: 575; /* Any weight, no extra files needed */}font-weight: 100 900 is the variable-font-specific syntax, declaring the variable range for this axis. After that, any weight between 100–900 renders from the same file without triggering additional downloads.
7. Advanced: Fine-Grained Control with the Font Loading API
If the CSS techniques above aren’t enough, JavaScript’s CSS Font Loading API (document.fonts) gives you programmatic control. A common scenario: load only the fonts “actually used in the current viewport,” and load more when the user scrolls to a certain area:
const fonts = document.fonts;
// Check whether a font is readyasync function ensureFont(family, source) { const face = new FontFace(family, source, { weight: '400' });
// Register first, but the font file is downloaded only when actually used fonts.add(face);
try { await face.load(); // Explicitly trigger loading document.body.classList.add('font-ready'); } catch (e) { console.error('Font failed to load, falling back to system font', e); }}
ensureFont('MyFont', 'url(/fonts/myfont.woff2) format("woff2")');document.fonts.ready is a Promise that resolves after all fonts declared by @font-face have finished loading, and can be used for visual transitions before and after fonts are ready.
8. Summary
Stringing the above techniques into a complete optimization pipeline, ordered by priority from high to low:
- Reduce font size: Subsetting + keeping only WOFF2 + merging weights with variable fonts;
- Load fonts earlier:
preload(same-origin) +preconnect(cross-origin); - Face the wait gracefully: Use
font-display: swapfor body text,optionalfor icons and branding; - Load on demand:
unicode-range+ Font Loading API.
There’s no “silver bullet” for font optimization, but every bit makes an immediate difference: a 5 MB Chinese font, after subsetting, WOFF2 compression, and on-demand loading, often sees its actual transfer size drop to under 5% of the original. For users on mobile networks, that’s the difference between “5 seconds of white screen” and “instant load.”
References
- web.dev — Optimize WebFont loading
- MDN — font-display
- MDN — CSS Font Loading API
- Google Fonts — Self-hosting guide