FOUT
Understanding and preventing FOUT in both SSR and client-side rendered applications.
Flash Of Unstyled Text (FOUT)
FOUT is one of the most common yet overlooked performance issues that affects user experience. When custom fonts load after the initial render, users see a brief flash of unstyled or fallback text before the fonts are applied.
Why FOUT Matters
Even a 100ms flash of unstyled text can impact user perception and your Core Web Vitals scores, particularly Cumulative Layout Shift (CLS).
What is FOUT?
Flash Of Unstyled Text (FOUT) occurs when:
- Browser renders text with a system/fallback font
- Custom font loads asynchronously
- Browser repaints text with the custom font
This creates a visible "flash" that can be jarring for users.
FOUT vs FOIT vs FOFT
| Acronym | Full Name | Behavior |
|---|---|---|
| FOUT | Flash Of Unstyled Text | Shows fallback font, then swaps to custom font |
| FOIT | Flash Of Invisible Text | Shows invisible text until custom font loads |
| FOFT | Flash Of Faux Text | Shows system font with adjusted metrics to prevent layout shift |
Why FOUT Happens
Understanding the browser rendering pipeline helps explain why FOUT occurs:
The browser doesn't wait for custom fonts before rendering text. It renders immediately with available fallbacks, then repaints when custom fonts arrive.
SSR Solutions
Server-Side Rendering provides unique opportunities to prevent FOUT.
1. Critical CSS Inlining
Inline critical CSS to eliminate render-blocking requests:
/* Critical CSS - inline in HTML head */
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
font-weight: 400;
font-style: normal;
}
body {
font-family: 'Custom Font', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}2. Next.js Font Optimization
Next.js offers excellent font handling with automatic optimization:
// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
preload: true,
fallback: ['system-ui', 'sans-serif']
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={inter.variable}>
<body className={inter.className}>{children}</body>
</html>
)
}3. Preloading Critical Fonts
Give the browser a head start on font loading:
<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/critical.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
<!-- Preconnect to font CDNs -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />4. Font Display Strategies for SSR
Choose the right font-display strategy:
/* swap: Shows fallback immediately, swaps when font loads */
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2');
font-display: swap;
}
/* optional: Uses fallback if font loads too quickly */
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2');
font-display: optional;
}
/* fallback: Short blocking period, then fallback */
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2');
font-display: fallback;
}Client-Side Rendering Solutions
Client-side rendering requires different strategies to manage font loading.
1. Web Font Loader
Use a font loading library for precise control:
// FontLoader.jsx
import { useState, useEffect } from 'react'
export default function FontLoader({ children }) {
const [fontsLoaded, setFontsLoaded] = useState(false)
useEffect(() => {
const loadFonts = async () => {
try {
// Load custom fonts
const font = new FontFace('CustomFont', 'url(/fonts/custom.woff2)')
await font.load()
document.fonts.add(font)
// Add class to body when fonts are ready
setFontsLoaded(true)
document.body.classList.add('fonts-loaded')
} catch (error) {
console.error('Font loading failed:', error)
}
}
loadFonts()
}, [])
return (
<div className={fontsLoaded ? 'fonts-loaded' : 'fonts-loading'}>
{children}
</div>
)
}2. CSS Variable Approach
Use CSS variables for smooth transitions:
/* Base styles */
:root {
--font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* When fonts load */
.fonts-loaded {
--font-primary: 'Custom Font', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
body {
font-family: var(--font-primary);
transition: font-family 0.3s ease;
}3. JavaScript Font Detection
Detect font loading with the Font Face API:
// useFontLoader.js
import { useState, useEffect } from 'react'
export function useFontLoader(fontFamily, fontUrl) {
const [isLoaded, setIsLoaded] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
const font = new FontFace(fontFamily, `url(${fontUrl})`)
font.load()
.then(() => {
document.fonts.add(font)
setIsLoaded(true)
})
.catch((err) => {
setError(err)
console.error(`Failed to load font ${fontFamily}:`, err)
})
return () => {
// Cleanup if component unmounts
if (document.fonts.has(font)) {
document.fonts.delete(font)
}
}
}, [fontFamily, fontUrl])
return { isLoaded, error }
}4. Fade-in Transitions
Smooth the transition between fonts:
.fonts-loading {
opacity: 0.85;
}
.fonts-loaded {
opacity: 1;
transition: opacity 0.2s ease-in;
}Performance Monitoring
Track font loading impact on user experience:
// FontPerformanceMonitor.jsx
import { useEffect } from 'react'
export function FontPerformanceMonitor() {
useEffect(() => {
// Monitor First Contentful Paint
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime)
}
}
})
observer.observe({ entryTypes: ['paint'] })
// Monitor font loading times
document.fonts.ready.then(() => {
const fontLoadTime = performance.now()
console.log('All fonts loaded in:', fontLoadTime, 'ms')
})
return () => observer.disconnect()
}, [])
return null
}Best Practices & Prevention Checklist
Font Loading Strategies
- Use
font-display: swapfor custom fonts - Preload critical fonts above the fold
- Preconnect to font domains (Google Fonts, Adobe Fonts, etc.)
- Use modern font formats (WOFF2 > WOFF > TTF)
- Implement proper fallback font stacks with similar metrics
Performance Considerations
- Monitor First Contentful Paint (FCP)
- Track Largest Contentful Paint (LCP)
- Measure Cumulative Layout Shift (CLS)
- Audit font loading times with Lighthouse
- Test on slow network connections (3G, 4G)
Implementation Checklist
- Test with browser devtools throttling
- Verify fallback fonts are visually similar
- Ensure no layout shift when fonts load
- Implement loading states for font-dependent components
- Monitor real user metrics (RUM)
- Test across different browsers and devices
Font Optimization
- Subset fonts to include only needed characters
- Consider variable fonts for multiple weights/styles
- Use
unicode-rangeto specify character sets - Self-host critical fonts for better control
- Implement font caching strategies
Comparison of Font Loading Strategies
| Strategy | SSR Benefits | CSR Benefits | Implementation Complexity | Performance Impact |
|---|---|---|---|---|
font-display: swap | Excellent | Good | Low | Minimal |
| Critical CSS Inlining | Excellent | Fair | Medium | Low |
| Web Font Loader | Good | Excellent | Medium | Moderate |
| Next.js Font Optimization | Excellent | N/A | Low | Minimal |
| Self-hosting | Excellent | Good | High | Low |
Core Web Vitals Impact
FOUT can negatively impact your CLS (Cumulative Layout Shift) score. Monitor this metric when implementing font loading strategies. Use font-display: optional for non-critical fonts to eliminate layout shift entirely.
Testing Your Implementation
1. Browser DevTools
# Test with throttling in Chrome DevTools
# Network tab → Throttling → Slow 3G
# Reload and observe font loading behavior2. Lighthouse Audit
# Run Lighthouse performance audit
# Focus on: FCP, LCP, CLS scores
# Check "Webfont loading" opportunities3. Real User Monitoring
// Track font loading in production
document.fonts.ready.then(() => {
const fontLoadTime = performance.now()
// Send to analytics
if (typeof gtag !== 'undefined') {
gtag('event', 'font_load_time', {
value: Math.round(fontLoadTime),
event_category: 'performance'
})
}
})Conclusion
FOUT is a solvable problem with the right strategies:
- For SSR applications: Use Next.js font optimization with
font-display: swap - For CSR applications: Implement font loading detection with smooth transitions
- For all applications: Monitor performance and test on slow connections
By understanding the root cause and implementing appropriate solutions, you can eliminate FOUT and provide a smoother, more professional user experience.
Remember
The best font loading strategy depends on your specific use case. Always test different approaches and measure real-world performance before deciding on a final implementation.