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
- Font Blocking Period: After the browser detects a
@font-facerule, it delays text rendering if the font is not yet loaded. - Timeout Switch Period: After a timeout (typically 3 seconds), fallback fonts are used for display.
- 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 tofallback, 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
- Use WOFF2 format: 40% smaller than TTF and 30% smaller than WOFF.
- 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"
- 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:
- Immediate display of readable text (avoiding FOIT).
- Smooth font switching transitions (reducing FOUT impact).
- Minimized layout shifts (CLS optimization).
- 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.