Font Loading & Optimization
WOFF2: The Only Format You Need
Stop shipping multiple font formats. WOFF2 provides 30% better compression than WOFF (using Brotli internally), 50%+ smaller files than TTF/OTF, and has 97%+ browser support in 2026 — covering every browser that matters. Unless you have a specific legacy requirement (e.g., locked-down IE11 enterprise intranet), WOFF2 is all you need. Adding WOFF as a fallback is acceptable but increasingly unnecessary.
A real-world comparison with the Montserrat font: WOFF2 was 83KB, WOFF was 94KB, TTF was 225KB, EOT was 226KB, and SVG was 1.8MB. The difference is massive, especially multiplied across multiple weights and styles.
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: swap;
font-weight: 400;
font-style: normal;
}
Serve font files with long cache lifetimes (Cache-Control: max-age=31536000, immutable) and use versioned filenames for cache busting. Fonts rarely change, so they should almost always be served from cache on repeat visits.
Resources:
font-display: Controlling the Flash
When a web font hasn’t loaded yet, browsers face a choice: show nothing (Flash of Invisible Text — FOIT) or show a fallback font and swap later (Flash of Unstyled Text — FOUT). The font-display descriptor in @font-face gives you explicit control:
swap — zero block period, infinite swap period. The browser immediately shows text in the fallback font, then swaps to the custom font whenever it arrives. Best for most body text and headings. Text is always visible, but there’s a visible reflow when the swap happens. Google Fonts includes &display=swap in all embed URLs by default.
optional — extremely small block period (~100ms), zero swap period. The browser renders the fallback font almost immediately, and will only use the custom font if it arrives within that tiny window (or is already cached). On slow connections, the custom font is simply skipped. This is the most performance-friendly option — no layout shift, no late swap, and on subsequent visits the cached font loads instantly. Jeremy Wagner: “If you use fonts that are ‘nice to have’ but could ultimately do without, consider specifying optional.”
fallback — short block period (~100ms), short swap period (~3s). A compromise: shows fallback quickly, allows a swap within 3 seconds, then locks in whatever is displayed. Good for balancing brand typography with performance.
block — short block period (~3s), infinite swap. Hides text for up to 3 seconds waiting for the font. Use only for icon fonts where fallback text would be meaningless.
The recommendation for most sites: use font-display: swap for headings and brand text, font-display: optional for body text. This ensures text is always readable while preserving the brand experience where it matters most.
Resources:
- Controlling Font Performance with font-display — Chrome Developers
- font-display — MDN
- font-display — CSS-Tricks Almanac
Reducing Layout Shift: Font Metric Overrides & Fallback Matching
The most visible performance problem with web fonts isn’t load time — it’s the layout shift when the fallback font swaps to the custom font. If the fallback has different metrics (character width, line height, ascent/descent), text reflows and the page jumps, hurting CLS.
CSS @font-face descriptors let you adjust a fallback font’s metrics to closely match the custom font, virtually eliminating the shift:
/* Adjusted fallback that matches the custom font's metrics */
@font-face {
font-family: 'Adjusted Arial Fallback';
src: local(Arial);
size-adjust: 105%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brand-font.woff2') format('woff2');
font-display: swap;
}
body {
font-family: 'BrandFont', 'Adjusted Arial Fallback', sans-serif;
}
size-adjust scales the fallback font’s glyphs to match the custom font’s character width. ascent-override, descent-override, and line-gap-override control the font’s vertical metrics — ascent (height above baseline), descent (depth below), and line gap — matching line heights between fallback and custom fonts.
Tools for generating fallback metrics:
- Fafofal (Fabulous Font Fallbacks) at highperformancewebfonts.com — Stoyan Stefanov’s browser-based tool where you upload your WOFF2, select a local fallback, and visually align baselines with live preview. It generates the exact
@font-faceoverride declarations. Everything runs locally in the browser; fonts are not uploaded anywhere. - Font Style Matcher — Monica Dinculescu’s original tool for visually comparing fallback and custom font metrics.
- Malte Ubl’s Perfect-ish Font Fallback — Generates computed
size-adjustvalues. - Next.js
next/font— Automatically calculatessize-adjustfrom the actual font files, eliminating manual metric matching entirely. It also handles self-hosting, subsetting, andfont-displayconfiguration. - @capsizecss/core — Generates CSS that aligns text metrics programmatically, used by tools like Vercel’s font system.
Resources:
- Fafofal: Fabulous Font Fallbacks — highperformancewebfonts.com
- Creating Perfect Font Fallbacks — Katie Hempenius, Chrome Developers
- CSS size-adjust for @font-face — Adam Argyle, web.dev
- Fabulous Font Face Fallbacks — Perfplanet Advent Calendar 2024
- Fixing Layout Shifts Caused by Web Fonts — DebugBear
- Reducing Layout Shift with Custom Fallback Fonts — Speed Kit
Font Subsetting: Ship Only the Characters You Use
Most font files contain thousands of glyphs — Latin, Cyrillic, Greek, mathematical symbols, ligatures, and more. An English-language site typically uses only 200–300 unique characters. Subsetting removes everything else, often reducing file sizes by 50–70%.
unicode-range in @font-face tells the browser which character ranges a font file covers. The browser only downloads the file if characters in that range appear on the page:
/* Latin subset only — browser skips download if page has no Latin text */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212;
}
/* Cyrillic subset — only downloaded if page contains Cyrillic characters */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-cyrillic.woff2') format('woff2');
font-display: swap;
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
Google Fonts uses this split-by-unicode-range approach automatically — when you load a Google Font, you get multiple @font-face rules with different unicode-range values, and the browser only downloads the subsets needed for the page’s actual text content. If you self-host, replicate this pattern.
Subsetting tools:
pyftsubset(from fonttools) — the standard CLI tool. Example:pyftsubset font.ttf --flavor=woff2 --output-file=font-latin.woff2 --unicodes="U+0000-00FF"- Glyphhanger — analyzes your actual HTML/URL content and generates optimal subsets containing only the characters your site actually uses.
- Text-to-Unicode Range tool at highperformancewebfonts.com — paste your text and get the exact unicode range for your
@font-facerule. - google-webfonts-helper — provides pre-subset, self-hostable files for any Google Font.
- Wakamaifondue (wakamaifondue.com) — “What can my font do?” — drag-and-drop font analysis showing all available features, glyphs, and OpenType capabilities, helping you decide what to keep.
Limit font weights and styles. Every additional weight (light, regular, medium, semibold, bold, extrabold) is another file download. For most sites, 2–3 weights (regular, medium/semibold, bold) are sufficient. Use the CSS font-synthesis property to let the browser synthesize italic or bold variants rather than downloading separate files for rarely-used styles.
Resources:
- Text-to-Unicode Range — highperformancewebfonts.com
- Wakamaifondue — What Can My Font Do?
- Glyphhanger — GitHub
- google-webfonts-helper
- Font Subsetting Guide — the-sustainable.dev
- Subsetting Web Fonts — Walter Ebert
- FontDrop.info — Font Inspector
Variable Fonts: Multiple Styles in a Single File
Variable fonts contain continuous ranges of weight, width, slant, and other design axes in a single file, replacing the need for multiple static font files. Instead of loading regular.woff2 (20KB), medium.woff2 (20KB), bold.woff2 (21KB), and italic.woff2 (22KB) — four files, four requests, ~83KB — a variable font covers all these in one file (~50KB, one request).
@font-face {
font-family: 'Inter Variable';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900; /* Full weight range */
font-style: normal;
font-display: swap;
}
/* Use any weight in the continuous range */
h1 { font-weight: 750; } /* Between bold and extrabold */
body { font-weight: 400; } /* Regular */
.caption { font-weight: 350; } /* Between light and regular */
Variable fonts shine when you need 3+ weights or styles, animation between weights (e.g., hover effects), or fine-grained typographic control. They also eliminate the need for font-synthesis since the browser can generate any intermediate weight from the continuous axis.
When NOT to use variable fonts: If you only need 1–2 weights, static fonts are often smaller. A variable font supporting weights 100–900 might be 50KB, while two static weights total 30KB. Always compare total bytes.
For variable fonts, use format('woff2-variations') or the newer format('woff2') tech(variations) syntax. Browsers that don’t support variable fonts also don’t support WOFF2, so no additional fallback format is needed.
Resources:
- Variable Fonts Guide — web.dev
- Variable Fonts — MDN
- v-fonts.com — Variable Fonts Showcase
- Axis-Praxis — Variable Font Playground
Self-Hosting vs. Google Fonts CDN: The Privacy and Performance Calculus
Self-hosting is recommended for most sites in 2026. Barry Pollard’s thorough analysis at tunetheweb.com breaks down exactly why.
The waterfall problem: When you load Google Fonts via the standard <link> embed, the browser must: (1) download your HTML, (2) discover the Google Fonts CSS link, (3) open a connection to fonts.googleapis.com and download the CSS, (4) parse the CSS to discover the actual font URLs on fonts.gstatic.com, (5) open another connection to that domain, and (6) finally download the font. On a 3G connection, Pollard measured 3 full seconds of overhead before the font even started downloading — all wasted on DNS lookups, TLS handshakes, and the two-domain request chain.
Cache partitioning killed the CDN argument: The supposed benefit of Google Fonts was that if a visitor loaded “Inter” from Google on another site, it’d be cached for yours. Since 2020, all major browsers partition HTTP caches by top-level domain (Safari led, Chrome and Firefox followed). Every site now pays the full download cost regardless of what fonts the user loaded elsewhere.
What Google Fonts does well (that you must replicate if self-hosting): Google Fonts returns browser-specific CSS — the CSS served to Chrome includes only WOFF2 references, while older browsers get WOFF. It splits fonts by unicode-range (Latin, Latin Extended, Cyrillic, etc.) so browsers only download the subsets they need. It includes font-display: swap by default. And it returns a preconnect Link header for fonts.gstatic.com. If you self-host, replicate the unicode-range splitting and font-display settings — don’t just dump a monolithic font file.
If you still use Google Fonts, at minimum add the preconnect hint before the stylesheet link — Pollard’s tests show this saves ~1 second on 3G by opening the fonts.gstatic.com connection in parallel:
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
rel="stylesheet">
Self-hosting advantages: eliminates two third-party connections (~100–300ms saved), enables long Cache-Control: max-age=31536000, immutable headers, allows aggressive subsetting for your specific content, ensures GDPR/privacy compliance (Google Fonts sends visitor IP addresses to Google — a German court ruled this violates GDPR in 2022), and removes a single point of failure (Google Fonts can be blocked by corporate proxies or entire countries like China).
Preload critical fonts — the font the browser needs for above-the-fold text — in the document <head>:
<link rel="preload" href="/fonts/brand-font-latin.woff2"
as="font" type="font/woff2" crossorigin>
The crossorigin attribute is required even for same-origin fonts (fonts are always fetched using CORS). Without it, the browser downloads the font twice. Only preload 1–2 critical fonts — over-preloading pushes other resources down the priority queue. And don’t preload every unicode-range subset — only preload the one you’ll actually paint above the fold.
Service Worker caching: pre-cache WOFF2 files at install time. First visit uses the preloaded font; subsequent visits serve it from the service worker in ~0ms. As Jono Alderson notes, “this is an easy win that almost nobody takes.”
Resources:
- Should You Self-Host Google Fonts? — Barry Pollard, Tune The Web
- Self-Host Your Static Assets — Harry Roberts, CSS Wizardry
- google-webfonts-helper — Self-Hosting Tool
- Preload Key Requests — web.dev
- Modern Font Loading Strategies — OpenReplay
- You’re Loading Fonts Wrong — Jono Alderson
Retiring Icon Fonts: Use SVG Instead
Icon fonts (Font Awesome loaded as a font, Bootstrap Glyphicons, Ionicons) were a clever hack a decade ago, but in 2026 they are technically indefensible. They cause accessibility problems (screen readers announce private-use Unicode characters as gibberish), they download the entire icon set even if you use 5 icons, they can’t be multicolored, they sometimes render incorrectly at certain sizes, and they add a render-blocking font load.
Replace icon fonts with inline SVG or an SVG sprite sheet. SVGs are semantically meaningless (won’t confuse screen readers), individually tree-shakeable (only ship icons you use), styleable with CSS (including currentColor for theming), and can be multicolored. Libraries like lucide-react, @heroicons/react, and react-icons provide tree-shakeable SVG icon components where each icon adds only its own bytes to the bundle.
For existing Font Awesome users, the official SVG + JS approach or individual SVG imports replace the font-based loading entirely.
Resources: