Web Font Loading: The Hidden Performance Killer
A practical Fusebox guide to web font loading.
Web Font Loading: The Hidden Performance Killer
Published: January 2024
Reading time: 7 minutes
Fonts can make or break your site's performance. A slow-loading font causes layout shifts, invisible text, and frustrated users. Here's how to analyze font loading and fix common problems.
Why Font Loading Matters
The Problem: FOIT & FOUT
FOIT (Flash of Invisible Text)
1. Page loads
2. Text is invisible (waiting for font)
3. Font loads (3 seconds later)
4. Text suddenly appears
5. User already left
FOUT (Flash of Unstyled Text)
1. Page loads with system font
2. Custom font downloads
3. Text suddenly changes style
4. Layout shifts
5. User loses reading position
Both hurt user experience and Core Web Vitals scores.
How Fonts Load
The Loading Timeline
HTML Parse → CSS Parse → Font Request → Download → Parse → Render
↓ ↓ ↓ ↓ ↓ ↓
0ms 50ms 200ms 800ms 850ms 900ms
Critical issue: Fonts only download when needed (text rendered).
Font Loading Triggers
/* Font defined */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
}
/* Font NOT downloaded yet */
/* Font used = download triggers */
body {
font-family: 'CustomFont', sans-serif;
}
Analyzing Font Performance
1. Network Analysis
// Find all font requests
performance.getEntriesByType('resource').forEach(resource => {
if (resource.name.includes('.woff') ||
resource.name.includes('.ttf') ||
resource.name.includes('.otf')) {
console.log(`Font: ${resource.name}`);
console.log(` Size: ${(resource.transferSize / 1024).toFixed(1)}KB`);
console.log(` Load time: ${resource.duration.toFixed(0)}ms`);
}
});
Common findings:
Font: /fonts/heavy-display.woff2
Size: 248.5KB // Too large!
Load time: 1847ms // Too slow!
Font: /fonts/body-text.woff2
Size: 42.3KB // Good
Load time: 234ms // Acceptable
2. Font Display Strategy
/* Bad: Default (FOIT) */
@font-face {
font-family: 'MyFont';
src: url('font.woff2');
/* No font-display = invisible text */
}
/* Good: Show text immediately */
@font-face {
font-family: 'MyFont';
src: url('font.woff2');
font-display: swap; /* FOUT but text visible */
}
/* Better: Progressive enhancement */
@font-face {
font-family: 'MyFont';
src: url('font.woff2');
font-display: optional; /* Use if loads fast */
}
3. Font Loading Patterns
Pattern 1: Google Fonts (Slow)
<link href="https://fonts.googleapis.com/css2?family=Roboto" rel="stylesheet">
Issues:
- DNS lookup for googleapis.com
- CSS download
- DNS lookup for fonts.gstatic.com
- Font download
- Total: 4 round trips!
Pattern 2: Self-Hosted (Faster)
<link rel="preload" href="/fonts/roboto.woff2" as="font" crossorigin>
<style>
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto.woff2') format('woff2');
font-display: swap;
}
</style>
Benefits:
- No external DNS
- Preloaded
- Single round trip
Real-World Font Issues
Example 1: News Website
// Font analysis results:
{
"serif-headline.woff2": {
size: "187KB",
loadTime: "1240ms",
usage: "5 headlines only"
},
"serif-body.woff2": {
size: "156KB",
loadTime: "980ms",
usage: "All article text"
},
"sans-ui.woff2": {
size: "134KB",
loadTime: "890ms",
usage: "Navigation only"
}
}
// Total: 477KB of fonts!
Problems:
- Heavy font for minimal usage
- No subsetting
- No variable fonts
Solution:
/* Use system fonts for UI */
.nav { font-family: -apple-system, system-ui, sans-serif; }
/* Subset headline font */
@font-face {
font-family: 'Headline';
src: url('headline-subset.woff2'); /* 23KB vs 187KB */
unicode-range: U+0020-007E; /* Basic Latin only */
}
/* Variable font for body */
@font-face {
font-family: 'BodyText';
src: url('body-variable.woff2'); /* One file, all weights */
}
Example 2: E-commerce Site
// Found 12 font files!
'OpenSans-Light.woff2': 44KB
'OpenSans-Regular.woff2': 48KB
'OpenSans-Medium.woff2': 49KB
'OpenSans-SemiBold.woff2': 48KB
'OpenSans-Bold.woff2': 49KB
'OpenSans-ExtraBold.woff2': 50KB
// ... 6 more italic variants
// Total: 580KB of fonts
Fix: Variable fonts
@font-face {
font-family: 'OpenSans';
src: url('OpenSans-Variable.woff2'); /* 92KB total! */
font-weight: 300 800;
}
/* Use any weight */
.light { font-weight: 300; }
.regular { font-weight: 400; }
.bold { font-weight: 700; }
Example 3: Icon Fonts
// Icon font analysis
'fontawesome.woff2': 156KB
Usage: 8 icons out of 1,500+
// 98% of font unused!
Modern solution: SVG icons
<!-- Instead of -->
<i class="fa fa-heart"></i>
<!-- Use -->
<svg class="icon">
<use href="#icon-heart"></use>
</svg>
Font Optimization Techniques
1. Preload Critical Fonts
<!-- Preload most important font -->
<link rel="preload"
href="/fonts/critical.woff2"
as="font"
type="font/woff2"
crossorigin>
Impact: Font starts downloading immediately, not when CSS processes.
2. Font Subsetting
# Create subset with only used characters
pyftsubset font.ttf \
--unicodes="U+0020-007E" \
--output-file="font-subset.woff2"
# Original: 248KB
# Subset: 31KB (87% smaller!)
3. Variable Fonts
/* Old way: Multiple files */
font-weight: 300; /* Light.woff2 */
font-weight: 400; /* Regular.woff2 */
font-weight: 700; /* Bold.woff2 */
/* New way: One variable font */
@font-face {
font-family: 'Variable';
src: url('font-variable.woff2');
font-weight: 300 900;
}
4. System Font Stack
/* Use system fonts for body text */
body {
font-family:
-apple-system, /* macOS/iOS */
BlinkMacSystemFont, /* macOS Chrome */
'Segoe UI', /* Windows */
Roboto, /* Android */
'Helvetica Neue', /* Older macOS */
sans-serif; /* Fallback */
}
/* Custom fonts only for branding */
h1 {
font-family: 'BrandFont', serif;
}
5. Font Loading API
// Load fonts programmatically
const font = new FontFace('MyFont', 'url(/fonts/font.woff2)');
font.load().then(() => {
document.fonts.add(font);
document.body.classList.add('fonts-loaded');
});
// CSS
.fonts-loaded {
font-family: 'MyFont', sans-serif;
}
Debugging Font Issues
Quick Audit Script
// Paste in console
(function auditFonts() {
// Get all @font-face rules
const fontFaces = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
fontFaces.push({
family: rule.style.fontFamily,
src: rule.style.src,
display: rule.style.fontDisplay || 'auto'
});
}
}
} catch (e) {}
}
console.log('Found fonts:', fontFaces);
// Check font-display
fontFaces.forEach(font => {
if (font.display === 'auto' || !font.display) {
console.warn(`⚠️ ${font.family} missing font-display!`);
}
});
// Check performance
const fontResources = performance.getEntriesByType('resource')
.filter(r => r.name.match(/\.(woff2?|ttf|otf)$/));
console.log('\nFont Performance:');
fontResources.forEach(font => {
const name = font.name.split('/').pop();
const size = (font.transferSize / 1024).toFixed(1);
const time = font.duration.toFixed(0);
if (font.transferSize > 100000) {
console.warn(`❌ ${name}: ${size}KB (too large!)`);
} else {
console.log(`✅ ${name}: ${size}KB in ${time}ms`);
}
});
})();
Browser DevTools
Chrome Font Tab:
- DevTools → Rendering tab
- Check "Disable local fonts"
- See exactly when fonts load
Network Waterfall:
- Filter by "Font"
- Check load timing
- Look for blocking
Font Loading Checklist
Performance
- Use WOFF2 format (best compression)
- Subset fonts (remove unused glyphs)
- Preload critical fonts
- Set proper font-display
- Consider variable fonts
- Limit font weights/styles
Strategy
- System fonts for body text?
- Custom fonts for headings only?
- Icon fonts → SVG icons?
- Self-host vs. Google Fonts?
- Fallback font similar to custom?
Testing
- Test on slow 3G
- Check layout shift (CLS)
- Verify text remains visible
- Test fallback fonts
- Monitor font file sizes
Common Mistakes
1. Loading Unused Fonts
/* Loaded but never used */
@font-face {
font-family: 'NeverUsed';
src: url('font.woff2');
}
2. Missing Crossorigin
<!-- Wrong: CORS error -->
<link rel="preload" href="font.woff2" as="font">
<!-- Right -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
3. Wrong Format Order
/* Bad: Old format first */
src: url('font.ttf') format('truetype'),
url('font.woff2') format('woff2');
/* Good: Modern format first */
src: url('font.woff2') format('woff2'),
url('font.woff') format('woff');
The Bottom Line
Font performance impacts:
- User experience (invisible/jumping text)
- Core Web Vitals (CLS, LCP)
- Conversion rates (users leave slow sites)
Optimize fonts like images - they're often the heaviest assets after images.
Analyze font loading instantly: Fusebox shows all web fonts, their sizes, load times, and optimization opportunities while you browse. $29 one-time purchase.