Why Web Fonts Matter More Than You Think

Custom web fonts are everywhere. They shape brand identity, improve readability, and make your site look polished. But here’s the uncomfortable truth: fonts are one of the most common performance bottlenecks on the web.

The average web page loads 3–5 font files totaling 100–300 KB. That might not sound like much, but fonts are render-blocking resources. Until the browser knows how to display text, it has to make a decision: show something else, or show nothing at all.

That decision is at the heart of two acronyms every web developer should understand — FOUT and FOIT — and one CSS property that puts you back in control: font-display.

In this guide, we’ll break down exactly what happens when fonts load slowly, compare every font-display value with real-world use cases, and share the optimization playbook we use at Lueur Externe to shave seconds off page load times.


Understanding the Font Loading Lifecycle

Before diving into FOUT and FOIT, it helps to understand the three phases every web font goes through:

  1. Block period — The browser has detected that a custom font is needed but hasn’t finished downloading it. During this phase, text is rendered with an invisible fallback (the glyphs exist in the DOM but are transparent).
  2. Swap period — The block period has ended, but the font still hasn’t arrived. The browser now renders text with a visible fallback (system) font. When the custom font finally loads, it swaps in.
  3. Failure period — If the font never loads, the browser keeps the fallback font permanently.

The duration of each phase is what differs between FOUT, FOIT, and each font-display value. Understanding this is the key to making informed decisions.


FOIT: Flash of Invisible Text

What Happens

FOIT stands for Flash of Invisible Text. When a browser uses FOIT behavior, it hides all text that depends on a custom font until that font has downloaded. If the download takes 3 seconds, users see a blank space for 3 seconds.

Most modern browsers (Chrome, Firefox, Edge, Safari) default to a 3-second FOIT timeout. After 3 seconds, they fall back to a system font — effectively switching to FOUT behavior. But 3 seconds of invisible text is an eternity on the web.

Why It’s Problematic

  • Users on slow connections (3G, spotty Wi-Fi) may see no readable content for several seconds.
  • It directly harms Largest Contentful Paint (LCP) if the largest element on the page is a text block.
  • Users may think the page is broken and leave — increasing bounce rate.

When You Might Accept It

In rare cases — icon fonts, for example — showing the wrong character (a square or random letter) is worse than showing nothing. FOIT-like behavior makes sense there.


FOUT: Flash of Unstyled Text

What Happens

FOUT stands for Flash of Unstyled Text. The browser immediately renders text in a fallback system font (like Arial or Times New Roman) while the custom font downloads in the background. Once the font arrives, the text re-renders with the custom typeface.

The Trade-Off

FOUT keeps content accessible from the very first paint. That’s a clear win for usability. But the visual “flash” when fonts swap can be jarring, and if the fallback font has different metrics (character width, line height, ascender/descender ratios), it can cause layout shifts — measured by the Cumulative Layout Shift (CLS) metric in Core Web Vitals.

A study by the HTTP Archive found that font-related CLS contributes to roughly 15–20% of total layout shift on the median web page.

Why Most Experts Prefer It

Despite the flash, FOUT is almost always the better default:

  • Content is readable immediately.
  • Users can start consuming information while the custom font loads.
  • It’s more accessible for screen readers and assistive technologies.
  • With the right techniques (see below), you can minimize or eliminate the visual disruption.

The font-display Property: Taking Back Control

Introduced in the CSS Font Loading Module Level 3, the font-display descriptor goes inside your @font-face rule and tells the browser exactly how to handle the block, swap, and failure periods.

Here’s the syntax:

@font-face {
  font-family: 'MyCustomFont';
  src: url('/fonts/mycustomfont.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* <-- this is the magic line */
}

Comparing All Five Values

The following table summarizes how each value behaves:

ValueBlock PeriodSwap PeriodBest For
autoBrowser default (~3s)IndefiniteLegacy behavior (not recommended)
blockShort (~3s)IndefiniteIcon fonts, symbol fonts
swapExtremely short (~100ms)IndefiniteBody text, headings — content-first approach
fallbackVery short (~100ms)Short (~3s)Balanced approach — swap only if font loads fast
optionalNone (0ms)NonePerformance-critical pages, optional branding fonts

Let’s dig into each one.

font-display: auto

This defers to the browser’s default strategy, which is typically FOIT with a 3-second timeout. Since you’re giving up control, we don’t recommend using auto on production sites.

font-display: block

The browser hides text for up to ~3 seconds, then swaps in the custom font whenever it arrives. This is the closest to pure FOIT behavior.

Use it for: Icon fonts (e.g., Font Awesome used as icons), where showing a random letter “A” or “X” instead of a hamburger menu icon would confuse users.

font-display: swap

This is the most commonly recommended value. The browser shows the fallback font almost immediately (the block period is ~100ms) and swaps to the custom font whenever it loads — even if that takes 10 seconds.

Use it for: Most text content — headings, body copy, navigation labels. It guarantees readability from the first frame.

/* Recommended for most websites */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-display: swap;
}

font-display: fallback

A compromise between swap and optional. The block period is ~100ms (like swap), but the swap period is limited to roughly 3 seconds. If the custom font hasn’t loaded within that window, the browser sticks with the fallback for the rest of the page’s lifetime.

Use it for: Situations where you want the custom font if it loads quickly, but you’d rather avoid a late layout shift if it doesn’t.

font-display: optional

The most aggressive performance option. There is essentially no block or swap period. The browser gives the font roughly 100ms to load. If it doesn’t make it, the fallback font is used permanently (though the custom font may be cached for the next page load).

Use it for: Performance-obsessed projects where CLS must be near zero. Google’s own documentation recommends optional for the tightest LCP and CLS scores.


Practical Optimization Strategies

Using font-display is step one. Here are the additional techniques that the web performance team at Lueur Externe combines to deliver sub-second font rendering for clients across e-commerce (PrestaShop, WooCommerce) and content platforms (WordPress).

Preload Critical Fonts

Preloading tells the browser to start downloading a font file before it discovers the @font-face rule in CSS. This can shave 200–500ms off load time on typical connections.

<link
  rel="preload"
  href="/fonts/inter-var.woff2"
  as="font"
  type="font/woff2"
  crossorigin="anonymous"
/>

Important tips:

  • Only preload 1–2 of your most critical font files. Preloading everything defeats the purpose.
  • Always include the crossorigin attribute, even for same-origin fonts. Fonts require CORS.
  • Use WOFF2 format — it offers 30% better compression than WOFF.

Subset Your Fonts

Most font files contain glyphs for Latin, Cyrillic, Greek, Vietnamese, and other scripts. If your site is only in English and French, you can subset the font to include only the characters you need.

Tools like glyphhanger, pyftsubset (from fonttools), or Google Fonts’ text parameter can do this:

# Using pyftsubset to create a Latin-only subset
pyftsubset Inter.ttf \
  --output-file=Inter-latin.woff2 \
  --flavor=woff2 \
  --layout-features='*' \
  --unicodes=U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD

Subsetting can reduce a 250 KB font file to under 30 KB. That’s a massive saving.

Self-Host Your Fonts

Relying on third-party CDNs like Google Fonts introduces:

  • An extra DNS lookup (~50–150ms)
  • A TCP/TLS connection to fonts.gstatic.com (~100–200ms)
  • Potential GDPR compliance issues (a German court fined a website €100 for using Google Fonts in January 2022)

Self-hosting eliminates all of these. Download the fonts, serve them from your own domain or CDN, and set aggressive caching headers:

# Nginx configuration for font caching
location ~* \.(woff2?|ttf|otf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Access-Control-Allow-Origin "*";
}

Use size-adjust to Minimize Layout Shift

One of the most powerful modern techniques is the CSS size-adjust descriptor, which scales the fallback font to match the custom font’s metrics as closely as possible:

/* Fallback font adjusted to match Inter's metrics */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
}

Tools like Fontaine or Next.js’s built-in @next/font automate this process. The result? Near-zero CLS from font swapping.

Limit the Number of Font Files

Every font weight and style is a separate HTTP request. A common mistake is loading:

  • Light (300)
  • Regular (400)
  • Medium (500)
  • Semi-Bold (600)
  • Bold (700)
  • Extra-Bold (800)
  • Plus italic variants for each

That’s potentially 12+ font files. Instead:

  • Use a variable font that contains all weights in a single file (often 50–100 KB in WOFF2).
  • Limit yourself to 2–3 weights if you’re using static fonts.
  • Avoid italic variants unless your design truly requires them — use font-synthesis as a compromise.

A Complete Font Loading Strategy: Step by Step

Here’s the full playbook we recommend, combining everything above:

  1. Choose WOFF2 variable fonts — one file for all weights.
  2. Subset to only the character sets your audience needs.
  3. Self-host the files on your own server or CDN.
  4. Preload the primary font file in <head>.
  5. Set font-display: swap (or optional for maximum performance).
  6. Create a metric-matched fallback using size-adjust, ascent-override, etc.
  7. Cache aggressively with immutable and a 1-year expiry.
  8. Test with Lighthouse, WebPageTest, and Chrome DevTools’ Font timeline.

Following these steps, it’s realistic to bring font-related rendering delays down from 1.5–3 seconds to under 200ms.


Real-World Impact: Numbers That Matter

Let’s put some concrete data behind these optimizations:

  • Google Fonts vs. self-hosted: A site loading three Google Fonts files saved 420ms on LCP simply by switching to self-hosted WOFF2 files with preloading. (Source: Lueur Externe internal client audit, 2024)
  • Variable fonts: Switching from six static font files (348 KB total) to one variable font (78 KB WOFF2) reduced total font payload by 77%.
  • font-display: optional impact: On a WordPress blog with 80% mobile traffic, switching from auto to optional reduced CLS from 0.18 to 0.03 — well within Google’s “good” threshold of 0.1.
  • Subsetting: A PrestaShop store serving customers in France reduced its primary font from 245 KB to 28 KB by removing Cyrillic, Greek, and Vietnamese subsets — an 88% reduction.

Common Mistakes to Avoid

Even experienced developers trip on these pitfalls:

  • Preloading too many fonts. Preload only the font(s) needed above the fold. Preloading 5 files creates resource contention.
  • Forgetting crossorigin on <link rel="preload">. The font will be fetched twice — once without CORS (discarded) and once with it.
  • Using font-display: block for body text. This creates FOIT. Your paragraphs will be invisible for up to 3 seconds on slow connections.
  • Not testing on real devices. Fonts render differently on Windows (ClearType), macOS (Core Text), and Android (FreeType). Test everywhere.
  • Loading fonts from multiple origins. Each origin requires a separate connection. Consolidate onto one domain.

How Frameworks Handle Font Loading

If you’re using a modern framework, you may already have built-in font optimization:

  • Next.js (@next/font): Automatically self-hosts Google Fonts, applies size-adjust, and sets font-display: swap by default.
  • Nuxt 3 (@nuxt/fonts): Similar auto-optimization with built-in subsetting.
  • WordPress (6.4+): Core now applies font-display: fallback to theme fonts registered through the Fonts API.
  • PrestaShop: Requires manual configuration — at Lueur Externe, we apply these optimizations as part of our PrestaShop performance audits.

Conclusion: Fonts Don’t Have to Be a Performance Tax

Custom web fonts are not going away — and they shouldn’t. Typography is a fundamental part of design and branding. But loading fonts carelessly costs you real performance, real Core Web Vitals scores, and ultimately real conversions.

The good news is that the tools available today — font-display, size-adjust, variable fonts, WOFF2 subsetting, and preloading — give you everything you need to deliver beautiful typography without sacrificing speed.

Start by adding font-display: swap to every @font-face rule on your site. Then work through the optimization playbook above. The impact on LCP and CLS alone will justify the effort.

If you’d rather have experts handle it, the team at Lueur Externe has been optimizing web performance since 2003 — across WordPress, PrestaShop, and custom stacks. We’ll audit your fonts, your Core Web Vitals, and your entire frontend pipeline to make sure your site loads as fast as it looks.

Ready to make your website faster? Get in touch with Lueur Externe for a free performance audit.