Build a decision framework for web font strategy: when to use system fonts, variable fonts, or subsetted static fonts — balancing brand needs, performance budgets, and Lighthouse scores.
Every font file is a trade: you pay in bytes, network requests, and potential layout shift to get brand expression and typographic quality. The question is never 'should I use custom fonts?' but 'which fonts are worth paying for, and what is the cheapest way to pay?' This lesson gives you the decision framework, the performance budget, and the audit skills to answer that question confidently on any project.
Start with the question: does this project require custom brand typography? If no — use system-ui. Zero download, instant render, excellent quality on all platforms. This is the right answer for internal tools, admin dashboards, developer tools, and any application where typography is functional rather than expressive. If yes — does the project use one typeface or two? For one typeface: a variable font is almost always the correct choice. One WOFF2 file, full weight range, zero faux bold risk. For two typefaces: use a variable font for the body typeface (loaded with swap), a display font with font-display: optional (avoids late swap), both subsetted to Latin only unless you need extended character support. Only load additional weights you actually use in the design. If in doubt, audit: use Chrome DevTools Coverage panel (Ctrl+Shift+P → Show Coverage) to see how much of each loaded font is actually used on the page.
1/* Decision 1: system only (no brand requirement) */
2body { font-family: system-ui, sans-serif; }
3/* Result: 0KB downloaded, instant, 0 CLS */
4
5/* Decision 2: one variable font (brand body) */
6@font-face {
7 font-family: 'Inter';
8 src: url('/fonts/inter-var-latin.woff2') format('woff2');
9 font-weight: 100 900;
10 font-display: swap;
11 unicode-range: U+0000-00FF; /* Latin only */
12}
13/* Result: ~100KB, swap, minimal CLS with good fallback */
14
15/* Decision 3: display + body variable fonts */
16/* Body: swap + preload */
17/* Display: optional (no late swap) */A font performance budget defines the maximum total font payload and maximum number of font network requests. Web Almanac data shows the median web page loads 3 font files totalling 100KB. For Lighthouse 90+ performance score: keep total font payload under 100KB (WOFF2, all subsets combined), keep font requests to 2 or fewer above the fold, eliminate any font request on the critical path that is not preloaded. The budget per project type: landing page / marketing (where brand typography is highest value): 150KB total, 2 files, both preloaded. Application / SaaS UI: 80KB total, 1 file, preloaded. Documentation / blog: 120KB total, 2 files, body preloaded. Internal tools: 0KB — use system-ui. Measure actual payload in Chrome DevTools Network panel filtered by Font, after disabling cache (Ctrl+Shift+R).
1Font Budget Calculator:
2
3Landing page budget: 150KB total
4- Inter Variable (Latin subset): ~85KB
5- Fraunces Variable (Latin subset, display only): ~60KB
6- Total: 145KB ✓ Under budget
7
8Subset Inter to reduce further:
9- Full Latin Inter Variable: 85KB
10- Latin-only subset (pyftsubset): ~38KB
11- Saving: 47KB — worth the build step1/* Cache-busting during audit: force fresh download */
2/* In DevTools Network: right-click → Clear browser cache */
3/* Filter: Font — see actual sizes and waterfall timing */
4
5/* After audit: verify with Lighthouse */
6/* Target metrics: LCP < 2.5s, CLS < 0.1, TBT < 200ms */CLS (Cumulative Layout Shift) from font loading occurs when the custom font's metrics differ from the fallback font's metrics, causing text blocks to reflow when the swap happens. The zero-CLS pattern combines three techniques: (1) font-display: optional — browser uses fallback if font is not cached within the block period, preventing a late swap entirely. (2) Preload — moves font loading to the earliest possible point, maximising the chance the font is ready before first paint, so optional does not fall back. (3) Size-adjust descriptor — scales the fallback font to match the custom font's metrics, so even if a swap does occur, the size difference is minimal. size-adjust is a percentage that scales the fallback's glyphs to match the target font's rendered size.
1/* Zero-CLS pattern: preload + optional + size-adjust fallback */
2
3/* Step 1: Preload in HTML head */
4/* <link rel="preload" href="/fonts/inter-var.woff2" */
5/* as="font" type="font/woff2" crossorigin /> */
6
7/* Step 2: font-display: optional on @font-face */
8@font-face {
9 font-family: 'Inter';
10 src: url('/fonts/inter-var.woff2') format('woff2');
11 font-weight: 100 900;
12 font-display: optional; /* No late swap if not cached */
13}
14
15/* Step 3: size-adjust fallback to match Inter metrics */
16@font-face {
17 font-family: 'InterFallback';
18 src: local('Arial'); /* Use system Arial as base */
19 /* Scale Arial to match Inter's metrics */
20 size-adjust: 107%; /* Inter renders slightly larger than Arial */
21 ascent-override: 90%; /* Match Inter's ascender height */
22 descent-override: 22%; /* Match Inter's descender */
23 line-gap-override: 0%; /* Inter has no line gap */
24}
25
26body {
27 /* InterFallback as second entry: nearly zero-CLS swap */
28 font-family: 'Inter', 'InterFallback', system-ui, sans-serif;
29}Before optimising, audit what is actually happening. Five audit steps: (1) Network panel — filter by Font, disable cache, hard reload. Note: number of requests, file sizes, start time relative to HTML request, and whether any font starts downloading before CSS. (2) Waterfall view — fonts starting after 500ms are good candidates for preload. (3) Coverage panel — how much of each font file is used? Unused glyphs = candidates for subsetting. (4) Lighthouse — check Eliminate render-blocking resources and Reduce unused CSS. (5) Web Vitals — CLS score directly reflects font swap impact; a CLS above 0.1 on first load often points to an unoptimised font-display strategy.
1// Performance API: measure font load time in production
2const observer = new PerformanceObserver(list => {
3 for (const entry of list.getEntries()) {
4 if (entry.initiatorType === 'css') {
5 // Note: fonts appear as 'css' initiator type
6 console.log('Resource:', entry.name, 'Duration:', entry.duration);
7 }
8 }
9});
10observer.observe({ type: 'resource', buffered: true });
11
12// Check if fonts are causing CLS
13const clsObserver = new PerformanceObserver(list => {
14 let clsValue = 0;
15 for (const entry of list.getEntries()) {
16 if (!entry.hadRecentInput) clsValue += entry.value;
17 }
18 console.log('CLS:', clsValue); // Target: < 0.1
19});
20clsObserver.observe({ type: 'layout-shift', buffered: true });A font strategy document justifies every loaded font file and defines the rules for the project. It prevents future font sprawl — the gradual accumulation of additional font files as designers add 'just one more' weight. It should answer: Which typeface(s) are we loading and why? Which weights and styles are we loading and why each is needed? Which loading strategy (swap/optional/preload)? What is the total payload budget? What is the fallback stack and has it been tested? Who approves adding a new font file? This is a living document, updated when the type system changes.
1## Font Strategy — ProjectName
2
3### Loaded Fonts
4| File | Size | Weights | display | Preloaded |
5|------|------|---------|---------|----------|
6| inter-var-latin.woff2 | 38KB | 100-900 | swap | Yes |
7| fraunces-var-latin.woff2 | 52KB | 100-900 | optional | No |
8
9### Budget
10- Total payload: 90KB (budget: 150KB) ✓
11- Requests above fold: 1 preloaded ✓
12- CLS contribution: < 0.05 ✓
13
14### Fallback Stacks
15- Body: 'Inter', 'InterFallback', system-ui, sans-serif
16- Display: 'Fraunces', Georgia, serif
17
18### Not Loaded (Rationale)
19- No separate italic file: Inter italic is synthesised acceptably
20 at display sizes; editorial pages load italic if needed.
21- No Extra Bold (800): Not in approved design systemProduction-ready font loading combining preload, optional display, and size-adjusted fallback for zero Cumulative Layout Shift.
Final lesson assembles everything into a complete typography design system — tokens, loading strategy, scale, and documentation — as a reusable project template.
Your progress is saved in this browser, on this device only.