Engineering Playbook

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:

  1. Browser renders text with a system/fallback font
  2. Custom font loads asynchronously
  3. Browser repaints text with the custom font

This creates a visible "flash" that can be jarring for users.

FOUT vs FOIT vs FOFT

AcronymFull NameBehavior
FOUTFlash Of Unstyled TextShows fallback font, then swaps to custom font
FOITFlash Of Invisible TextShows invisible text until custom font loads
FOFTFlash Of Faux TextShows 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: swap for 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-range to specify character sets
  • Self-host critical fonts for better control
  • Implement font caching strategies

Comparison of Font Loading Strategies

StrategySSR BenefitsCSR BenefitsImplementation ComplexityPerformance Impact
font-display: swapExcellentGoodLowMinimal
Critical CSS InliningExcellentFairMediumLow
Web Font LoaderGoodExcellentMediumModerate
Next.js Font OptimizationExcellentN/ALowMinimal
Self-hostingExcellentGoodHighLow

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 behavior

2. Lighthouse Audit

# Run Lighthouse performance audit
# Focus on: FCP, LCP, CLS scores
# Check "Webfont loading" opportunities

3. 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:

  1. For SSR applications: Use Next.js font optimization with font-display: swap
  2. For CSR applications: Implement font loading detection with smooth transitions
  3. 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.