Fast Font Loading Strategies
Our homepage text was invisible for 1.2 seconds while two WOFF2 files downloaded from Google Fonts. Lighthouse marked it as render-blocking. We self-hosted a subsetted variable font, added preload, set font-display: swap with a metric-matched fallback, and text appeared instantly with a barely noticeable swap 300ms later. LCP dropped by 900ms.
font-display strategies
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
| Value | Behavior | Use when |
|---|---|---|
swap |
Fallback immediately, swap when loaded | Body text (recommended default) |
optional |
Use font only if cached/loaded quickly | Non-critical decorative fonts |
fallback |
Brief invisible period (~100ms), then swap | Headings where brief FOIT is acceptable |
block |
Invisible until loaded (up to 3s) | Avoid — causes FOIT |
Preloading critical fonts
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>
The crossorigin attribute is required — fonts loaded via @font-face use CORS mode. Place preload in <head> before CSS that references the font.
Self-hosting vs. CDN
Self-host for performance and privacy:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
body {
font-family: 'Inter', system-ui, sans-serif;
}
Benefits over Google Fonts CDN: no third-party DNS lookup, full control over subsetting, no GDPR tracking concerns, and same-origin caching with your other assets.
Subsetting
Include only the characters you need:
pyftsubset Inter-Regular.woff2 \
--output-file=Inter-Latin.woff2 \
--unicodes=U+0020-007F,U+00A0-00FF,U+2013-2014
For English-only sites, Latin subset reduces file size by 60-80%. Add character ranges when you expand to other languages.
Fallback font matching
Prevent layout shift by matching fallback metrics:
@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', system-ui, sans-serif;
}
Tools like Fallback Font Generator calculate override values automatically.
Variable fonts
One file, all weights:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
Compare: four static WOFF2 files at ~50KB each = 200KB total. One variable file = ~80KB.
Impact on Core Web Vitals
Fonts interact directly with LCP when the LCP element is text rendered in a web font. If the browser waits for the font before painting text, LCP delays by the full font download time. font-display: optional can improve LCP by allowing the browser to skip the web font entirely on slow connections. font-display: swap improves LCP versus block but may cause CLS if fallback and web font metrics differ — use size-adjust overrides to minimize shift.
Measure before and after with Web Vitals field data. Font changes that look fine locally on fast connections may still cause FOIT on 3G if preload is missing.
Loading strategy checklist
- Self-host WOFF2 files on your CDN
- Subset to required character ranges
- Preload the primary font used above the fold
- Set
font-display: swapon all @font-face declarations - Match fallback metrics with size-adjust overrides
- Use variable fonts to reduce file count
- Limit font families — two typefaces maximum for most sites
Google Fonts self-hosting workflow
Download from google-webfonts-helper or google-fonts npm packages. Host WOFF2 on your CDN. You keep the same font files without the extra DNS + connection to fonts.googleapis.com. Update @font-face paths and remove the external stylesheet link.
FOUT vs. CLS tradeoff
font-display: optional eliminates swap-induced CLS entirely but may show fallback on slow connections permanently. For brand-critical headings, accept a small CLS budget (≤0.05) with metric-matched fallbacks rather than skipping the web font.
Measuring success in production
Deploy changes behind feature flags when possible so you can compare metrics between control and treatment groups. Use Real User Monitoring to capture performance data from actual devices and network conditions — lab tools alone miss the long tail of user experiences. Set up alerts for regressions: a 10% LCP increase week-over-week warrants investigation before it hits CrUX.
Document your baseline metrics before making changes. Performance work without measurement is guesswork. Share results with the team — concrete numbers ("LCP improved 800ms on mobile") build support for continued investment in web performance and reliability.
Review changes quarterly. Browser updates, new API support, and traffic pattern shifts can obsolete previous optimizations or create new opportunities. What worked in 2024 may not be the best approach in 2026.
Additional production considerations
Teams often underestimate the maintenance cost of performance optimizations. Automate what you can: CI bundle budgets, Lighthouse CI on PRs, and RUM dashboards that alert on regressions. Manual audits don't scale past a handful of pages.
Security and performance intersect more than teams expect. Third-party scripts that hurt INP also expand your attack surface. Self-hosting fonts and critical assets reduces both latency and supply-chain risk. Review every external dependency quarterly — remove what you no longer need.
Accessibility and performance share goals: semantic HTML helps screen readers and gives the browser better rendering hints. Native elements like dialog, popover, and details reduce JavaScript while improving accessibility. Prefer platform features over custom implementations when they meet your requirements.
Mobile users dominate traffic for most sites. Test on real mid-tier Android hardware, not just desktop Chrome. Simulated throttling in DevTools approximates network conditions but not CPU constraints. A fix that helps desktop may be invisible on mobile if the bottleneck is JavaScript execution, not network.
Collaborate with backend teams on TTFB and API response times. Frontend optimizations can't fix a 2-second server response. Set SLAs for API endpoints that feed critical pages and measure them in the same RUM pipeline as Core Web Vitals.
Resources
- web.dev: Best practices for fonts
- font-display (MDN)
- Google Fonts knowledge base
- Fallback Font Generator
- Variable fonts guide (web.dev)
Frequently asked questions
What is the difference between FOIT and FOUT?
FOIT (Flash of Invisible Text) hides text until the web font loads, showing blank space. FOUT (Flash of Unstyled Text) shows fallback text immediately, then swaps to the web font when loaded. font-display: swap prevents FOIT by always showing fallback text first. The tradeoff is a visible font swap that can cause layout shift if fallback metrics differ.
Should I preload all web fonts?
Preload only fonts needed for above-the-fold content — typically one or two weights of your primary typeface. Over-preloading competes with LCP resources for bandwidth. Use rel=preload with crossorigin for fonts loaded via @font-face. Don't preload fonts used only below the fold or in modals.
How do variable fonts affect performance?
Variable fonts combine multiple weights and styles into a single file, reducing HTTP requests and total byte size. One variable font file at 80KB can replace four static files totaling 200KB. They also enable smooth weight animations. The tradeoff is slightly higher parsing cost, which is negligible on modern devices.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →