Web Fonts Performance: Subsetting, font-display, and Preloading
A technical guide to web fonts performance — formats, subsetting, font-display, preloading, variable fonts, and the patterns that eliminate FOIT and FOUT.

Web fonts are one of the most under-optimised performance leaks on modern sites. A custom font that "looks great" can silently add 100 to 400 ms to LCP, cause noticeable Cumulative Layout Shift, and create the dreaded flash of invisible text. Most sites we audit have web font issues that take an afternoon to fix and deliver measurable Core Web Vitals improvements.
This guide covers web fonts end to end. Format selection, subsetting, font-display, preloading, variable fonts, and the patterns that eliminate FOIT (flash of invisible text) and FOUT (flash of unstyled text) while keeping LCP fast. By the end you will have a clear playbook for handling fonts on any site.
The work is precise and small. Done right, font optimization is one of the cleanest performance wins available.
Why web fonts hurt performance
Web fonts add weight, latency, and rendering complications.
Weight
A single custom font file can be 50 to 400 KB. Most sites use 2 to 4 weights × multiple styles, totaling 150 to 1,500 KB just for fonts.
Latency
Fonts are typically loaded after CSS parsing. The fonts block text rendering or cause text to flash to the fallback font.
Layout shift
When the custom font loads and replaces the fallback, surrounding text often shifts position due to different metrics. This contributes to CLS.
Render blocking
Without proper handling, fonts can delay LCP by hundreds of milliseconds.
We covered the broader CLS dynamics in our Core Web Vitals deep dive guide. Fonts are usually a contributor to all three Core Web Vitals.
Section 1 — Font format selection
The first decision and the easiest win.
Use WOFF2
WOFF2 is the modern web font format. 30 to 50 percent smaller than WOFF, supported in all browsers since 2016. There is no reason to use anything else for modern projects.
@font-face {
font-family: "Inter";
src: url("inter.woff2") format("woff2");
font-display: swap;
}
Skip WOFF, TTF, and EOT
Older formats are larger and unnecessary. Only ship them if you have specific legacy browser support requirements (which is rare in 2026).
Variable fonts
A variable font contains multiple weights and styles in a single file. Instead of shipping 4 files for Regular, Medium, Bold, Italic, one file covers them all.
@font-face {
font-family: "Inter";
src: url("inter-variable.woff2") format("woff2-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
A variable font is typically 30 to 60 percent smaller than the equivalent multi-file static font set.
For Inter, Roboto, and many popular fonts, variable versions are available and widely supported.
Section 2 — Subsetting
Most fonts contain glyphs for hundreds of languages. You probably only use one or two scripts.
What subsetting does
Subsetting removes glyphs you do not use. A full Inter file is 200+ KB. Latin-only subset is around 30 KB.
Common subsets
- Latin: Western European languages (English, French, Spanish, etc.)
- Latin Extended: covers more European languages (Polish, Czech, Vietnamese)
- Cyrillic: Russian, Ukrainian, Bulgarian
- Arabic: Arabic-script languages
- Hebrew: Hebrew-script languages
- Greek: Greek-script
- CJK: Chinese, Japanese, Korean (large, often custom-subset)
Subsetting with Google Fonts
When using Google Fonts via the API:
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap&subset=latin" rel="stylesheet">
For self-hosted fonts, use a tool like fonttools or online subsetters to generate subset files.
Subsetting with next/font
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
});
next/font handles subsetting automatically.
We covered the next/font setup in our Next.js performance best practices guide.
Multi-language sites
For sites supporting multiple scripts (like our own EN/FR/AR site), subset per script and load conditionally:
@font-face {
font-family: "Inter";
src: url("inter-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("inter-arabic.woff2") format("woff2");
unicode-range: U+0600-06FF;
font-display: swap;
}
The browser loads only the file for characters actually used on the page.
Section 3 — font-display
The font-display descriptor controls how text renders before, during, and after the font loads.
The values
auto: browser default. Behaves like block. Don't use.
block: text is invisible while font loads (up to 3 seconds), then swaps to custom font. The FOIT pattern. Avoid.
swap: text uses fallback font immediately. Swaps to custom font when loaded. The FOUT pattern. Most common choice.
fallback: brief invisible period (100 ms), then uses fallback. Custom font swaps only if it loads quickly (3 seconds). After that, fallback stays.
optional: 100 ms invisible period, then uses fallback. Custom font is loaded but only applied if it was cached. Best for performance, worst for design consistency.
Recommendation
swap for most cases. Text renders immediately in fallback. Swaps when custom font is ready.
@font-face {
font-family: "Inter";
src: url("inter.woff2") format("woff2");
font-display: swap;
}
optional when LCP performance matters more than custom font being applied (some content sites, especially on slow connections).
Never use block or auto in production.
Section 4 — Eliminating layout shift on font swap
font-display: swap causes FOUT but doesn't eliminate the layout shift when the font swaps. Browser CSS metrics adjustments fix this.
The problem
Fallback font (Arial) has slightly different metrics than custom font (Inter). When the swap happens, line heights and character widths shift, causing CLS.
The fix — size-adjust, ascent-override, descent-override, line-gap-override
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: "Inter", "Inter Fallback", sans-serif;
}
This creates a "fallback font" that mimics Inter's metrics using Arial. When the custom font loads, the swap is invisible because the metrics match.
Calculating the right values
The Fontaine tool and Fontaine CLI calculate these values automatically. Run against your custom font and get the matching CSS.
For Next.js, next/font does this automatically. Other frameworks need manual setup.
Section 5 — Preloading critical fonts
For fonts used above the fold, preloading cuts font load time significantly.
The preload tag
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
This tells the browser to fetch the font immediately, parallel with HTML and CSS parsing.
When to preload
- Fonts used in above-the-fold text
- The font used by the LCP element (if it's text)
- Critical font weights (typically Regular and Bold)
When NOT to preload
- Italic or extra-weight variants used only in body text
- Decorative fonts used below the fold
- Fonts conditionally loaded based on user preferences
Each preload "steals" bandwidth from other resources. Preload only what is critical.
crossorigin requirement
The crossorigin attribute is required even for same-origin fonts. Missing it causes the browser to fetch the font twice.
Section 6 — Self-hosting vs Google Fonts CDN
The hosting choice affects performance and privacy.
Self-hosting
Pros:
- One less third-party connection (no preconnect to fonts.googleapis.com)
- GDPR-friendly (no data sent to Google)
- Better cache control
- Can subset aggressively
Cons:
- More setup work
- Need to track font updates
- More bytes on first visit (no shared cache benefit)
Google Fonts CDN
Pros:
- Zero setup
- Automatic format negotiation
- Shared browser cache (used to be a benefit, less now with cache partitioning)
Cons:
- Extra DNS lookup + TLS to fonts.googleapis.com
- GDPR concerns in EU contexts
- Less control over caching
Recommendation for 2026
Self-host for production sites. Use next/font if on Next.js (which self-hosts automatically). Use a tool like google-webfonts-helper to download Google Fonts for self-hosting.
For prototypes and personal projects, Google Fonts CDN is fine.
Section 7 — Common font weights and styles
Which weights and styles do you actually need?
Minimum set
For most marketing sites:
- Regular (400)
- Bold (700)
That's it. 2 font files cover 90 percent of typography needs.
Expanded set
If you genuinely need more:
- Regular (400)
- Medium (500) — for slightly bolder body
- Semibold (600) — for subheadings
- Bold (700) — for headings
4 files is the maximum most sites should ship.
Italic and special styles
Italic adds another file per weight. Skip italic if you can use CSS font-style: italic to synthesise it.
Display variants (light, ultralight, extrabold, black) are rarely used outside specialist design. Don't ship them by default.
Variable fonts make this moot
If you use a variable font, all weights live in a single file. Use any weight you want without shipping multiple files.
Section 8 — Font fallback chains
Even with custom fonts, your fallback chain matters.
A solid fallback chain
body {
font-family: "Inter", "Inter Fallback", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
This says: use Inter if loaded, then the metric-matched Inter Fallback (if defined), then system fonts in order of preference.
The system font stack at the end ensures text always renders even if Inter and Inter Fallback both fail.
System font stack
For sites prioritising performance over brand identity, use only system fonts:
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
Zero font files to load. Native UI feel. Often the right choice for content-heavy sites.
A 14-day font optimization plan
If your fonts are unoptimised, follow this sequence.
Days 1 to 2 — Audit. List every font file your site loads. Note formats, weights, and total weight.
Days 3 to 5 — Reduce. Keep only weights you actually use. Switch to variable font if available. Convert to WOFF2.
Days 6 to 8 — Subset. Subset to needed character ranges. Use unicode-range for multi-script sites.
Days 9 to 11 — Display and preload. Add font-display: swap everywhere. Preload critical fonts. Add metric-matched fallbacks.
Days 12 to 13 — Self-host. If currently using Google Fonts CDN, migrate to self-hosted with next/font or similar.
Day 14 — Measure. Compare LCP, CLS, and total font weight to baseline.
Most sites cut font weight 40 to 70 percent and improve LCP by 100 to 400 ms in this window.
A real example — Lyon medical practice fonts
We took over a Lyon medical practice site loading 6 font files totaling 480 KB. Audit revealed: full Google Fonts CDN delivery (no subsetting), 6 weights when only 2 were actually used, no font-display, font-related CLS contribution of 0.07.
After 5 days — switched to next/font with Inter variable subset, only Latin range, swap display, metric-matched fallback — font weight dropped to 32 KB. LCP improved 280 ms. CLS contribution dropped to 0.01. The full story is in our Lyon medical practice case study.
Common font optimization mistakes
These are the patterns we see most often.
Loading 6+ font weights. Most sites need 2 to 4 weights max.
No font-display directive. Defaults to auto/block, causing FOIT.
No subsetting. Loading Cyrillic glyphs you do not use is wasted bytes.
No metric-matched fallback. Causes CLS on font swap.
Loading Google Fonts via CDN in production. Extra DNS, TLS, no subsetting control.
Old formats (TTF, WOFF) instead of WOFF2. 30 to 50 percent more bytes for no benefit.
Not preloading critical fonts. Free LCP improvement, often skipped.
Skipping variable fonts where available. Significant size reduction available.
Frequently asked questions
Should I use Google Fonts in production?
Self-hosting is better for production. next/font does this automatically for Next.js. For other frameworks, use google-webfonts-helper to download and self-host.
What is the best font-display value?
swap for most cases. Text renders immediately in fallback, swaps to custom font when loaded. Better than block (FOIT) for performance.
How do I prevent CLS from font swap?
Use metric-matched fallback fonts via size-adjust, ascent-override, and descent-override CSS properties. next/font handles this automatically.
Should I use variable fonts?
Yes when available. Variable fonts are 30 to 60 percent smaller than equivalent multi-file font sets. Inter, Roboto, and many popular fonts have variable versions.
How many font weights should I ship?
For most sites: 2 (Regular, Bold). For sites with more typography needs: 4 (Regular, Medium, Semibold, Bold). More than 4 is rarely justified.
Is system font stack a good alternative?
Yes for content-heavy sites where brand consistency matters less than performance. Zero font load, native UI feel, fastest possible rendering.
Get a font optimization audit
We audit web fonts free of charge. Within 48 hours we deliver a per-font breakdown of weight, optimization opportunities, and expected LCP and CLS impact.
Book a free 30-minute audit. We screen-share, walk through your font setup, and you leave with a clear action plan.
Or explore our Web Development service for the full system we run on performance-focused client accounts.
Want these strategies applied to your business?
30 minutes of free audit with concrete recommendations tailored to your business.
Read next
The Lighthouse Audit Checklist: 50 Points We Check on Every Site
A comprehensive Lighthouse audit checklist — performance, accessibility, best practices, SEO. The 50-point list we run on every web performance engagement.
Third-Party Script Management: How to Stop Tags From Killing Your Site
A guide to managing third-party scripts — Google Tag Manager, chat widgets, analytics, marketing pixels. Strategies for deferring, replacing, and removing scripts.
CDN Selection Guide 2026: Cloudflare, Vercel, Bunny, Fastly, and Beyond
A practical CDN selection guide for 2026 — Cloudflare, Vercel, Bunny CDN, Fastly, AWS CloudFront. Features, pricing, edge compute, and which fits your stack.