Back to Blog
Performance
10 min read

Web Font Loading Best Practices: Speed and Visual Stability

Custom fonts make the web beautiful, but they come at a cost. A poorly loaded font can block rendering, cause invisible text, or trigger jarring layout shifts. Here is how to get the best of both worlds: beautiful typography and fast, stable pages.

The Font Loading Problem: FOIT vs FOUT

When a browser encounters a custom web font it has never seen before, it faces a dilemma. The HTML and CSS have arrived, the layout is ready to paint, but the font file is still downloading. The browser has two choices, and neither is perfect.

FOIT (Flash of Invisible Text) is the default behavior in most browsers. Text rendered in the custom font becomes invisible while the font downloads. If the font takes more than about three seconds, the browser falls back to a system font. Users stare at a blank space where content should be, which feels broken.

FOUT (Flash of Unstyled Text) is the alternative: the browser immediately shows text in a fallback system font, then swaps in the custom font once it finishes loading. Content is always visible, but the swap causes a visible reflow as letter widths and line heights change.

Neither outcome is ideal, but FOUT is almost always preferable. Visible content in a fallback font is far better than invisible content. The rest of this article focuses on making FOUT as smooth as possible and, in some cases, eliminating the swap entirely.

The font-display Property

The font-display descriptor in your @font-face rule gives you direct control over the browser's behavior during font loading. Each value represents a different trade-off between speed and visual fidelity.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-display: swap;
}
  • auto - The browser decides. Usually behaves like block. Not recommended because you have no control.
  • block - Invisible text for up to 3 seconds, then falls back. Causes FOIT. Only appropriate for icon fonts where showing a wrong glyph would be confusing.
  • swap - Immediately shows the fallback font, swaps when the custom font loads. Guarantees visible text but may cause layout shift. Best for body text and headings.
  • fallback - Very short invisible period (~100ms), then shows the fallback. The custom font only swaps in if it arrives within about 3 seconds. Good balance between visual consistency and performance.
  • optional - Extremely short invisible period. The browser may choose not to swap at all if the font arrives too late, using the fallback for the entire page visit. Best for performance-critical pages where the custom font is a nice-to-have.

For most sites, font-display: swap is the right starting point. If you care deeply about CLS scores and have properly tuned fallback metrics, font-display: optional is the gold standard.

Preloading Critical Fonts

By default, font files are only discovered when the browser parses CSS. That means the download starts late, after the HTML, CSS, and any blocking scripts have been fetched. Preloading moves font discovery to the earliest possible moment.

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

The crossorigin attribute is required even when the font is self-hosted on the same domain. Without it, the browser will fetch the font twice: once from the preload and once from the CSS, because font requests are always CORS requests.

  • Only preload 1-2 fonts - Preloading too many fonts competes with other critical resources like CSS and JavaScript. Preload only the fonts used above the fold.
  • Preload WOFF2 only - WOFF2 has over 97% browser support. Preloading multiple formats wastes bandwidth.
  • Pair with font-display: swap - Preloading without font-display still causes FOIT. Always use both together.

Subsetting Fonts

A full font file for a typeface like Noto Sans can exceed 500 KB because it includes glyphs for every supported language. If your site is in English, you only need Latin characters, which brings the file size down to around 20-40 KB.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-latin.woff2') format('woff2');
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074,
    U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

The unicode-range descriptor tells the browser which characters this font file covers. If the page only uses characters in that range, the browser downloads just this file. You can split a font into multiple subsets (Latin, Cyrillic, Greek, CJK) and the browser will only download the ones it needs.

Tools like pyftsubset (from the fonttools Python library) or Google Fonts' built-in subsetting let you strip out unused glyphs. Use our Webfont Generator to convert and subset fonts with a visual interface.

Variable Fonts: One File, Multiple Weights

Traditional font loading requires a separate file for each weight and style. A typical setup with Regular, Medium, Semi-Bold, and Bold in both normal and italic means eight HTTP requests and eight separate files.

Variable fonts solve this by encoding an entire range of weights (and sometimes widths, slant, and other axes) into a single file. One variable font file replaces all your static weight files.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}

h1 { font-weight: 750; } /* Any value between 100-900 */
p { font-weight: 380; } /* Not just 400 or 300 */

The font-weight: 100 900 range declaration tells the browser this single file covers all weights from thin to black. You can then use any integer value in CSS, not just the traditional 100/200/.../900 steps.

A single Inter variable font file is approximately 100 KB, while loading four static weights separately would total around 160 KB plus the overhead of four HTTP requests. Variable fonts are supported in all modern browsers.

Self-Hosting vs Google Fonts

Google Fonts is convenient, but self-hosting your fonts is almost always faster. Here is why.

When you use Google Fonts, the browser must first download a CSS file from fonts.googleapis.com, which requires a DNS lookup and a TLS handshake to a third-party domain. Only after parsing that CSS does the browser discover the actual font files on fonts.gstatic.com, requiring a second DNS lookup and TLS handshake to yet another domain. This chain of connections adds 200-600ms of latency before the font even starts downloading.


<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet">


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

Self-hosted fonts are served from your own domain, so there are no extra connections. Combined with preloading, the font download can begin almost immediately. You also gain full control over caching headers, file format, and subsetting.

The old argument that Google Fonts benefited from cross-site caching (if a user visited another site with the same font, it would be cached) no longer applies. Modern browsers partition their cache by top-level domain for privacy, so a font cached from Site A is never reused on Site B.

Font Fallback Matching with CSS Descriptors

Even with font-display: swap and preloading, there will always be a brief moment where the fallback font is visible. The problem is that system fonts have different metrics than your custom font: different x-height, letter spacing, and ascender/descender proportions. When the custom font swaps in, lines reflow and the page jumps. This is the Cumulative Layout Shift (CLS) that hurts your Core Web Vitals score.

Modern CSS lets you tune the fallback font's metrics to closely match your custom font, minimizing the reflow on swap.

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107.64%;
  ascent-override: 90.49%;
  descent-override: 22.48%;
  line-gap-override: 0%;
}

body {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
}
  • size-adjust - Scales the fallback font's glyphs so that its average character width matches the custom font. This is the most impactful property.
  • ascent-override - Adjusts the space above the baseline to match the custom font's ascent, keeping line heights consistent.
  • descent-override - Adjusts the space below the baseline, preventing vertical shifts when the font swaps.
  • line-gap-override - Controls additional leading between lines. Set to 0% to remove default line gaps in the fallback font.

Next.js has built-in support for this. When you use next/font, it automatically calculates these override values and generates an optimized fallback font declaration.

Core Web Vitals Impact

Font loading directly affects two Core Web Vitals metrics.

Largest Contentful Paint (LCP) is delayed when fonts block rendering. A font loaded with font-display: block hides text for up to 3 seconds. If that text is the largest element on the page (a common case for headings), LCP is directly penalized. Using font-display: swap with preloading ensures text is visible immediately, so LCP measures the fallback text render time instead of waiting for the font.

Cumulative Layout Shift (CLS) increases every time the font swap causes text to reflow. A swap from Arial to Inter can shift every paragraph, heading, and button on the page. The fallback matching technique described above reduces this shift to near zero.




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


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

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107.64%;
  ascent-override: 90.49%;
  descent-override: 22.48%;
}

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

Putting It All Together

Here is a complete, production-ready font loading strategy that combines all the techniques covered in this article.

  • 1.Choose a variable font to reduce the number of files from many to one.
  • 2.Subset the font to only the character sets your site uses. Use our Webfont Generator to handle conversion and subsetting.
  • 3.Self-host the font on your own domain to eliminate third-party connection overhead.
  • 4.Preload the WOFF2 file in the document head so the browser starts downloading it immediately.
  • 5.Set font-display: swap (or optional) to prevent invisible text.
  • 6.Create a matched fallback using size-adjust and override descriptors to minimize layout shift on swap.

This combination gives you custom typography with minimal performance impact. The font loads as fast as possible, text is always visible, and when the swap happens, the layout barely moves. Your Largest Contentful Paint stays fast, and your Cumulative Layout Shift stays near zero.