Back to blog
PerformanceJanuary 1, 2024· 5 min read

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:

  1. DevTools → Rendering tab
  2. Check "Disable local fonts"
  3. 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.