Home›Frontend Performance Optimization›Font Loading Performance
Frontend Performance Optimization

Font Loading Performance: font-display, Preloading, Subsetting, and Variable Fonts

Custom web fonts enhance visual identity but introduce performance costs that directly affect Core Web Vitals. A single font family with four weights (regular, medium, bold, italic) at 50-100KB per file adds 200-400KB to page weight. Worse, fonts block text rendering by default — users stare at invisible text (FOIT) or experience jarring font swaps (FOUT) that degrade both perceived performance and Cumulative Layout Shift.

Understanding FOIT and FOUT

When the browser encounters text styled with a web font that has not yet loaded, it faces a choice: render the text immediately using a fallback font (Flash of Unstyled Text — FOUT), or hide the text until the web font arrives (Flash of Invisible Text — FOIT). Without explicit instructions, browsers follow their default behavior: Chrome and Firefox show invisible text for up to 3 seconds before falling back, while Safari hides text indefinitely until the font loads.

Font Loading Behavior Comparison FOIT Invisible text (up to 3s) Fallback shown Web font rendered swap Fallback shown immediately Web font swaps in (may cause CLS) optional 100ms Uses whichever loaded first — no swap, no CLS

The font-display Property

The font-display descriptor in @font-face rules controls how the browser handles the loading period. It is the single most important font performance setting.

@font-face {
  font-family: 'Manrope';
  src: url('/fonts/manrope-var.woff2') format('woff2');
  font-weight: 200 800;
  font-display: swap;
}
  • font-display: swap — Shows fallback text immediately, swaps to web font when loaded. Good for body text where readability during loading is paramount. Causes a visible swap (FOUT) and potential CLS if the fallback and web font have different metrics.
  • font-display: optional — Gives the font a tiny window (~100ms) to load. If it misses the window, the fallback is used for the entire page load, and the downloaded font is cached for subsequent navigations. Zero CLS, zero FOUT — the best option for performance-critical pages.
  • font-display: fallback — A compromise: 100ms invisible block, then up to 3 seconds with fallback before committing to whatever is available. Balances FOIT duration with swap likelihood.
  • font-display: block — Up to 3 seconds of invisible text, then fallback. Appropriate only for icon fonts where fallback characters would be meaningless.

Choosing the Right Strategy

For most sites, use font-display: swap for headings (where brand identity matters) and font-display: optional for body text (where layout stability matters more). If you are using Google Fonts, append &display=swap to the stylesheet URL to set swap behavior for all loaded fonts.

Preloading Critical Fonts

Without preloading, the browser discovers font files late in the rendering pipeline: it must first download the HTML, parse the CSS, build the render tree, and only then request the font file used by the first rendered text. The preload link hint tells the browser to start downloading the font file immediately, in parallel with CSS parsing.

<head>
  <!-- Preload only the most critical font files -->
  <link rel="preload" href="/fonts/manrope-var.woff2"
        as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/fonts/public-sans-regular.woff2"
        as="font" type="font/woff2" crossorigin>

  <link rel="stylesheet" href="/styles.css">
</head>

The crossorigin attribute is required even for same-origin fonts because WOFF2 files are fetched using CORS. Omitting it causes the browser to download the font twice — once from the preload (without CORS) and again from the @font-face rule (with CORS). Preload only the fonts needed for above-the-fold text — typically one or two files. Preloading too many fonts wastes bandwidth on resources that may not be needed immediately.

Font Subsetting

Most Latin-script websites use fewer than 100 unique characters, yet a full font file contains 2,000-10,000 glyphs covering multiple scripts, ligatures, and special symbols. Subsetting strips unused glyphs from the font file, reducing sizes by 60-90%.

/* Google Fonts automatically subsets via unicode-range */
@font-face {
  font-family: 'Manrope';
  src: url('/fonts/manrope-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153,
                 U+02BB-02BC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
  font-display: swap;
}

The unicode-range descriptor enables the browser to download the font file only if characters within that range appear on the page. Google Fonts splits each font into multiple subset files by script (Latin, Cyrillic, Greek, CJK) and downloads only the subsets the page actually uses. For self-hosted fonts, tools like pyftsubset (from fonttools) and glyphhanger create custom subsets matching your actual content.

# Create a subset with only Latin characters + specific symbols
pyftsubset Manrope-Variable.woff2 \
  --output-file=manrope-latin.woff2 \
  --flavor=woff2 \
  --layout-features='kern,liga,calt' \
  --unicodes='U+0000-00FF,U+2000-206F,U+20AC'

# Result: 95KB → 28KB (70% reduction)

Variable Fonts

Traditional font families require separate files for each weight and style — Regular, Medium, SemiBold, Bold, Italic. A family with four weights and their italics ships eight font files. Variable fonts contain all weights (and optionally widths, slants, optical sizes) in a single file, typically 60-80KB for a Latin subset compared to 200-400KB total for equivalent static files.

/* Single variable font replaces 4+ static files */
@font-face {
  font-family: 'Manrope';
  src: url('/fonts/manrope-var.woff2') format('woff2');
  font-weight: 200 800;  /* continuous range */
  font-display: swap;
}

/* Use any weight, not just predefined steps */
h1 { font-weight: 750; }
h2 { font-weight: 650; }
h3 { font-weight: 580; }

Variable fonts also enable smooth weight animations and precise typographic tuning not possible with static fonts. One HTTP request loads all weights, reducing connection overhead and enabling faster rendering of the complete font family.

Fallback Font Matching

The font-display: swap strategy causes a visible shift when the web font replaces the fallback. The CSS size-adjust, ascent-override, descent-override, and line-gap-override descriptors tune the fallback font's metrics to match the web font, minimizing the visual shift and eliminating CLS from font swaps.

/* Adjusted system font fallback matching Manrope metrics */
@font-face {
  font-family: 'Manrope Fallback';
  src: local('Arial');
  size-adjust: 104.5%;
  ascent-override: 98%;
  descent-override: 24%;
  line-gap-override: 0%;
}

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

Next.js, Nuxt, and other meta-frameworks now generate these adjusted fallback declarations automatically. The @font-face metric overrides ensure that line heights, paragraph heights, and text wrapping remain identical between the fallback and web font, producing zero-CLS font loading even with font-display: swap.

Self-Hosting vs. Google Fonts CDN

Self-hosting fonts from your own domain eliminates the DNS lookup and TCP connection to fonts.googleapis.com and fonts.gstatic.com. On HTTP/2 or HTTP/3 (where your domain already has an open connection), self-hosted fonts start downloading immediately without the 100-300ms overhead of establishing a connection to a third-party CDN.

Google Fonts provides automatic subsetting, format negotiation, and CDN caching. However, privacy regulations (GDPR) in some jurisdictions consider Google Fonts' IP logging a compliance issue, making self-hosting a legal requirement regardless of performance. For self-hosting, download WOFF2 files from Google Fonts, apply custom subsetting, and serve them with long cache headers (Cache-Control: public, max-age=31536000, immutable).

Measuring Font Performance

Chrome DevTools Network panel filtered to "Font" shows all font requests, their sizes, and timing relative to other resources. The Performance panel's "Layout Shift" markers identify CLS caused by font swaps. Lighthouse specifically flags fonts without font-display, unpreloaded critical fonts, and excessive font file sizes.

The Web Font Loading API (document.fonts) provides programmatic access to font loading state, enabling advanced loading strategies like critical font preloading with progressive enhancement for secondary fonts.