Solutions for Optimizing Font Loading and FOIT/FOUT Issues

Solutions for Optimizing Font Loading and FOIT/FOUT Issues

Problem Description
Font loading optimization primarily addresses two core issues: FOIT (Flash of Invisible Text) and FOUT (Flash of Unstyled Text). When a webpage uses custom fonts, the browser behaves as follows before the font files are fully loaded:

  • FOIT: Keeps text invisible (default behavior in modern browsers).
  • FOUT: Initially displays fallback fonts, then switches after the font loads (traditional behavior in IE/Edge).
    Both phenomena can cause layout shifts and a poor user experience.

Detailed Optimization Steps

Step 1: Understanding the Font Loading Lifecycle

  1. Font Blocking Period: After the browser detects a @font-face rule, it delays text rendering if the font is not yet loaded.
  2. Timeout Switch Period: After a timeout (typically 3 seconds), fallback fonts are used for display.
  3. Font Application Period: Text is re-rendered after the font is fully loaded.
    The optimization goal is to reduce layout shifts and invisible text time by controlling these three stages.

Step 2: Choosing an Optimized Font Loading Strategy

/* Basic font definition */
@font-face {
  font-family: 'OptimizedFont';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* Key property: Avoids FOIT */
}

Optional values for the font-display property:

  • auto: Browser default behavior (usually causes FOIT).
  • block: Very short blocking period (about 3 seconds), then permanent replacement.
  • swap: No blocking period, immediately displays fallback fonts, and replaces them after loading.
  • fallback: Very short blocking period (about 100ms), followed by a brief fallback period.
  • optional: Similar to fallback, but may not switch fonts.

Step 3: Implementing Font Loading Monitoring and Precise Control

// Using the Font Loading API for fine-grained control
const font = new FontFace('OptimizedFont', 'url(font.woff2)');

document.fonts.add(font);

font.load().then(() => {
  // Add a CSS class after the font is loaded
  document.documentElement.classList.add('fonts-loaded');
  
  // Optionally cache the loading status with sessionStorage
  sessionStorage.setItem('font-loaded', 'true');
}).catch(error => {
  console.error('Font loading failed:', error);
});

// Corresponding control styles in CSS
.body-text {
  font-family: system-ui, sans-serif; /* Fallback fonts */
  transition: font-family 0.3s ease;
}

.fonts-loaded .body-text {
  font-family: 'OptimizedFont', system-ui, sans-serif;
}

Step 4: Optimizing the Font File Itself

  1. Use WOFF2 format: 40% smaller than TTF and 30% smaller than WOFF.
  2. Subset fonts: Include only the required character sets.
# Generate a subset using the pyftsubset tool
pyftsubset font.ttf --output-file=font-subset.woff2 --flavor=woff2 --text="ABCDEabcde12345"
  1. Subset segmentation using unicode-range:
/* Split font files for different languages */
@font-face {
  font-family: 'MultiLangFont';
  src: url('font-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF; /* Latin characters */
}

@font-face {
  font-family: 'MultiLangFont';
  src: url('font-cjk.woff2') format('woff2'); 
  unicode-range: U+4E00-9FFF; /* CJK characters */
}

Step 5: Preloading Critical Fonts

<!-- Preload critical above-the-fold fonts -->
<link rel="preload" href="critical-font.woff2" as="font" type="font/woff2" crossorigin>

<!-- Asynchronously load non-critical fonts -->
<link rel="preload" href="non-critical-font.woff2" as="font" type="font/woff2" crossorigin media="print" onload="this.media='all'">

Step 6: Complete Font Loading Optimization Solution

<!DOCTYPE html>
<html>
<head>
  <!-- Preload critical fonts -->
  <link rel="preload" href="primary-font.woff2" as="font" crossorigin>
  
  <style>
    /* System font fallback stack */
    :root {
      --font-stack: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    
    /* Font definition and loading strategy */
    @font-face {
      font-family: 'PrimaryFont';
      src: url('primary-font.woff2') format('woff2');
      font-display: swap;
      font-weight: 400;
    }
    
    /* Initially use system fonts */
    body {
      font-family: var(--font-stack);
      font-size: 16px;
      line-height: 1.5;
    }
    
    /* Optimized styles after font loading */
    .fonts-loaded body {
      font-family: 'PrimaryFont', var(--font-stack);
      /* Adjust letter spacing for the new font */
      letter-spacing: 0.02em;
    }
    
    /* Transition effect to hide unstyled text */
    .font-loading .text-content {
      opacity: 0;
    }
    
    .fonts-loaded .text-content {
      opacity: 1;
      transition: opacity 0.3s ease;
    }
  </style>
</head>
<body class="font-loading">
  <script>
    // Check if the font is already cached
    if (sessionStorage.getItem('primaryFontLoaded')) {
      document.documentElement.classList.add('fonts-loaded');
      document.body.classList.remove('font-loading');
    } else {
      // Asynchronously load the font
      const font = new FontFace('PrimaryFont', 'url(primary-font.woff2)');
      
      font.load().then(() => {
        document.fonts.add(font);
        document.documentElement.classList.add('fonts-loaded');
        document.body.classList.remove('font-loading');
        sessionStorage.setItem('primaryFontLoaded', 'true');
      }).catch(() => {
        // Fall back to system fonts if loading fails
        document.body.classList.remove('font-loading');
      });
      
      // Set a loading timeout (3 seconds)
      setTimeout(() => {
        document.body.classList.remove('font-loading');
      }, 3000);
    }
  </script>
</body>
</html>

Final Outcome
By implementing this combined solution, the following can be achieved:

  1. Immediate display of readable text (avoiding FOIT).
  2. Smooth font switching transitions (reducing FOUT impact).
  3. Minimized layout shifts (CLS optimization).
  4. Instant font display for subsequent page visits.

This optimization is particularly significant for content-based websites (e.g., news, blogs) and projects with high brand consistency requirements.