Type something to search...
How to Load Web Fonts Efficiently with font-display

How to Load Web Fonts Efficiently with font-display

Custom fonts give a site its personality, but they come with a loading problem. A font file is a separate download that often isn't discovered until the browser has parsed your CSS and found text that needs it. Until the file arrives, the browser has to decide what to do with that text: hide it, or show it in a fallback font and swap later. Get this wrong and you end up with a blank page where the headline should be, or text that visibly jumps and reflows a second after load.

The font-display descriptor is how you control that decision. This guide explains each value, when to use which, and how to combine font-display with preloading, subsetting, and fallback metric overrides so fonts load quickly without hurting your Largest Contentful Paint or Cumulative Layout Shift scores.

FOIT and FOUT

Two acronyms describe the two failure modes:

  • FOIT (Flash of Invisible Text): the browser hides text while the web font downloads. Users see empty space where content should be.
  • FOUT (Flash of Unstyled Text): the browser shows text in a fallback font immediately, then swaps to the web font when it arrives. Users can read right away, but the text may shift when the swap happens.

Neither is free. FOIT delays reading; FOUT can cause layout shift. font-display lets you choose the trade-off per font.

The Font Display Timeline

Every font load moves through up to three periods:

  1. Block period: if the font isn't loaded, text is rendered invisibly using a fallback. If the font arrives during this period, it's used straight away.
  2. Swap period: if the font isn't loaded, text is rendered in the fallback font. If the font arrives, the browser swaps it in.
  3. Failure period: the browser gives up on the web font for this page and keeps the fallback.

The font-display values are just different lengths for those periods.

The font-display Values

You set font-display inside an @font-face rule:

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

auto

Lets the browser decide. In practice most browsers behave like block. This is the default if you don't specify anything.

block

A short block period, usually around 3 seconds, followed by an unlimited swap period. Text is invisible for up to three seconds, then shows in the fallback until the font loads. Use it only for fonts where the fallback would be meaningless, like an icon font, where showing the underlying characters would be confusing.

swap

An extremely short block period (effectively zero) and an unlimited swap period. Text appears immediately in the fallback and swaps whenever the font loads, even if that's much later. This is the value most commonly recommended for body and heading fonts because text is never invisible. The downside is that a late swap can shift the layout.

fallback

An extremely short block period (around 100ms) and a short swap period (around 3 seconds). If the font doesn't arrive within that window, the fallback stays for the rest of the page view. The font is still downloaded and cached, so it'll be used on the next page. This is a good compromise for body text: users see content quickly, and late swaps that would cause jarring shifts are avoided.

optional

An extremely short block period and no swap period. If the font isn't available almost immediately, the browser uses the fallback for this page view and doesn't swap later. Browsers may also skip the download entirely on slow connections. Once cached, the font is used on subsequent pages.

optional gives the best layout stability, since there's never a late swap, and pairs very well with preloading. It's ideal when the font is a nice-to-have rather than essential to your brand.

Quick Comparison

ValueBlock periodSwap periodBest for
blockShort (about 3s)InfiniteIcon fonts
swapExtremely shortInfiniteHeadings, brand-critical text
fallbackExtremely short (about 100ms)Short (about 3s)Body text
optionalExtremely shortNonePerformance-first sites

Exact timings are left to the browser, so treat the numbers as approximate.

Using font-display with Google Fonts

Google Fonts adds font-display for you through the display URL parameter:

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap"
  rel="stylesheet"
/>

The preconnect hints start the connection to Google's servers early. That helps, but a third-party font still requires extra connections. Self-hosting the files on your own domain is usually faster, and it gives you full control over caching and preloading.

Preload the Fonts That Matter

The browser normally discovers fonts late: it has to download the HTML, then the CSS, then build the render tree and find text that uses the font. Only then does the font request start. A preload hint moves that request to the very beginning:

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

Three things to get right:

  • crossorigin is required, even for fonts on your own domain. Fonts are fetched in anonymous CORS mode, and without the attribute the preloaded file won't match the real request, so it's downloaded twice.
  • The URL must match exactly the one in your @font-face rule.
  • Preload only one or two files, typically the regular weight used for body text and maybe the heading font. Preloading every weight and style competes with more important resources like your CSS and hero image.

Preload combined with font-display: optional is a strong pattern. The preload makes it likely the font arrives within the tiny block window, and optional guarantees there's never a late swap if it doesn't.

Use WOFF2 and Subset Your Fonts

The smaller the file, the sooner it arrives.

Serve WOFF2

WOFF2 uses Brotli-based compression and is supported in every modern browser. There's no reason to ship TTF, OTF, or EOT to modern browsers anymore:

@font-face {
  font-family: "Source Serif";
  src: url("/fonts/source-serif-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: fallback;
}

Subset to the Characters You Need

A full font file might include thousands of glyphs for languages your site never displays. Subsetting removes them. The pyftsubset tool from fonttools is a common choice:

pip install fonttools brotli

pyftsubset SourceSerif-Regular.ttf \
  --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" \
  --layout-features="kern,liga" \
  --flavor=woff2 \
  --output-file=source-serif-latin.woff2

That keeps Basic Latin, Latin-1, and common punctuation and symbols, which covers most English and Western European text.

Split with unicode-range

If you do need several scripts, split the font into subsets and declare each with unicode-range. The browser downloads only the subsets whose characters actually appear on the page:

@font-face {
  font-family: "Source Serif";
  src: url("/fonts/source-serif-latin.woff2") format("woff2");
  font-display: fallback;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC, U+2122;
}

@font-face {
  font-family: "Source Serif";
  src: url("/fonts/source-serif-cyrillic.woff2") format("woff2");
  font-display: fallback;
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

An English page never requests the Cyrillic file.

Stop the Layout Shift: Tune Your Fallback

swap and fallback keep text visible, but the swap itself can shift content if the fallback font has different proportions. A paragraph set in Arial might wrap onto four lines, then five lines after Inter arrives, pushing everything below it down.

You can make the fallback match the web font closely using @font-face metric overrides:

  • size-adjust scales the fallback's glyphs.
  • ascent-override, descent-override, and line-gap-override adjust vertical metrics so line heights match.

The trick is to create a new font family that points at a local system font and adjusts it:

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

@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", system-ui, sans-serif;
}

The percentages above are illustrative. The correct values depend on the specific fonts. Tools such as Fontaine, the Capsize library, and online fallback font generators calculate them from the font files. Frameworks do this automatically too: Next.js's next/font generates an adjusted fallback for you when you load fonts through it.

With a well-tuned fallback, a swap changes the shape of the letters but barely moves the text, so layout shift is close to zero.

Metric overrides are supported in Chromium-based browsers and Firefox, and size-adjust works in Safari. Support for the other override descriptors in Safari has been more limited, so check current support. Browsers that ignore the overrides simply use the unadjusted fallback, which is no worse than not having one.

Reduce the Number of Font Files

Every weight and style is a separate download unless you use a variable font.

  • Audit what you actually use. Designs often call for five weights when two would do. Each one you drop is one fewer request.
  • Use a variable font when you need several weights. One variable file often replaces three or more static files and may be smaller than their combined size.
  • Consider a system font stack for body text. It loads instantly and looks native on every platform:
body {
  font-family:
    system-ui,
    -apple-system,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial,
    sans-serif;
}

A common approach is to use a custom font for headings, where personality matters most, and a system stack for body text.

Cache Fonts Aggressively

Font files rarely change, so serve them with a long cache lifetime and a versioned filename:

Cache-Control: public, max-age=31536000, immutable

When you update a font, change its filename (for example inter-var.v2.woff2) so returning visitors download the new version.

Common Mistakes

  • Missing crossorigin on preload, which causes a duplicate download.
  • Preloading fonts that aren't used above the fold, which delays more important resources.
  • Relying on auto, which usually means invisible text for up to three seconds on slow connections.
  • Using block for body text, trading readability for a font users won't miss for a second.
  • Loading many weights you don't use, often because a font service's embed code included them by default.

Putting It All Together

Here's a solid baseline for a self-hosted font setup:

<head>
  <link
    rel="preload"
    href="/fonts/inter-latin-var.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
  <link rel="stylesheet" href="/css/main.css" />
</head>
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC, U+2122;
}

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

:root {
  --font-sans: "Inter", "Inter Fallback", system-ui, sans-serif;
}

body {
  font-family: var(--font-sans);
}

One subsetted variable file, preloaded, with text visible immediately and a metric-matched fallback to keep layout stable during the swap.

Conclusion

Web fonts don't have to cost you speed or stability. font-display decides what happens while a font downloads: swap for fonts that must appear, fallback for a balanced approach on body text, and optional when layout stability and speed matter most. Combine it with a preload for your most important file, WOFF2 and subsetting to shrink downloads, and metric-adjusted fallbacks to minimize layout shift.

Do those few things and your text will appear immediately, look right once the font arrives, and stay put while it happens.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading