Web Font Loading: font-display, Preload and Subsetting

Fast font loading comes down to four moves: tell the browser about the font early (preconnect or preload), send small WOFF2 files that hold only the characters you need, choose a font-display value that suits the text, and size the fallback font to match so nothing jumps. Use font-display: optional when avoiding layout shift matters most.

font-display values at a glance
ValueText while loadingLate font arrivesGood for
swapFallback almost immediatelyAlways swapped inBody text that must show at once
optionalFallback almost immediatelyNot swapped in; fallback staysLowest layout shift
fallbackFallback almost immediatelySwapped in only within about 3sA middle ground
blockInvisible for up to about 3sAlways swapped inIcon fonts, logos set in type
autoBrowser decidesBrowser decidesLeaving it to the browser

Updated

Fonts are one of the few resources that can hide your text or shove the layout around. Drag the slider below to set how long a font takes to arrive and watch what each font-display value shows at every moment, then copy the loading patterns that keep pages steady.

font-display timeline simulator

Starting view: the font takes 1.5 seconds to load; drag the slider to change it. Grey is invisible text, amber the fallback, blue the web font.

Scale: 0 to 5 seconds. With optional, the spec lets the browser abort a late download or finish it at very low priority.

Spec-recommended periods
ValuePeriods
blockShort block (3s recommended), infinite swap
swapExtremely small block (100ms or less), infinite swap
fallbackExtremely small block, short swap (3s recommended)
optionalExtremely small block, no swap period

Self-hosted font with preload

Preload the one file the first screen needs

as, type and crossorigin must all be present. The @font-face rule sits inline in the head so it is discovered at once.

<link rel="preload" href="/fonts/inter-var-latin.woff2" as="font" type="font/woff2" crossorigin>
<style>
  @font-face {
    font-family: "Inter";
    src: url("/fonts/inter-var-latin.woff2") format("woff2");
    font-weight: 100 900;
    font-display: swap;
  }
  body { font-family: "Inter", system-ui, sans-serif; }
</style>

Split files with unicode-range

Latin and Cyrillic subsets of one family

Both rules share a family name. A page written only in English never downloads the Cyrillic file.

@font-face {
  font-family: "Brand";
  src: url("/fonts/brand-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2013-2014, U+2018-201E;
  font-display: swap;
}
@font-face {
  font-family: "Brand";
  src: url("/fonts/brand-cyrillic.woff2") format("woff2");
  unicode-range: U+0400-045F, U+0490-0491;
  font-display: swap;
}

A fallback sized to match

Adjusted local fallback to cut layout shift

The numbers are placeholders: measure your own font against the fallback and tune them until the two line up.

@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; }

Google Fonts with preconnect and a subset for one heading

The second stylesheet asks only for the letters in the logo text, which keeps that file tiny.

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:wght@700&text=Harbour%20Bakery&display=swap">

The font display timeline

When text needs a font that has not arrived, the browser walks through up to three periods. In the block period it lays the text out with an invisible fallback. In the swap period it shows the fallback and switches when the font lands. After that comes the failure period, where the fallback stays for good.

font-display only changes how long the first two periods last. The figures in the simulator are the ones the CSS Fonts Level 4 draft recommends; browsers may use slightly different timings.

Why late swaps cause layout shift

Even invisible text takes up space measured with the fallback font. When the real font replaces it, every line can get wider or narrower, so paragraphs rewrap and everything underneath moves. That movement is counted in Cumulative Layout Shift.

There are two cures. Avoid the late swap entirely with optional, or make the fallback occupy the same space as the web font using size-adjust and the metric override descriptors on a local() fallback face.

Subsetting

A font with thousands of glyphs is wasteful for a page that uses a hundred. Subsetting tools strip unused characters before you upload the file, and unicode-range lets the browser skip whole files it does not need.

For a short piece of display text, the Google Fonts API accepts a text= parameter and returns a file with just those characters. It is handy for a logo or a single heading set in a decorative face.

How it works

  1. Set the slider to the time your font takes to download on a slow connection.
  2. Read each row: grey is invisible text, amber is the fallback font, blue is the web font.
  3. Notice which values switch fonts late. A late switch is when visible text reflows.
  4. Copy the snippets for preload, unicode-range subsets and adjusted fallbacks.

Tips

Frequently asked questions

What does font-display: swap do?

It gives the font an extremely small block period, which the CSS Fonts spec recommends keeping at 100ms or less, and an infinite swap period. Text appears in the fallback font almost at once and switches to the web font whenever it finishes loading.

Is font-display: swap bad for CLS?

It can be. If the fallback and the web font have different widths or line heights, the late switch moves text and the content below it. Fix that with size-adjust and the ascent, descent and line-gap overrides on the fallback, or use optional.

Why does my preloaded font download twice?

Fonts are fetched in anonymous CORS mode, so the preload link needs the crossorigin attribute even when the file is on the same origin. Without it the preload does not match the real request and the font is fetched again.

Should I preload Google Fonts?

Usually not. web.dev warns that preloading bypasses some of the browser's own content negotiation, including unicode-range, so you might fetch a subset the page never uses. Preconnect to fonts.googleapis.com and fonts.gstatic.com instead, or self-host and preload your own file.

How does unicode-range save bytes?

When a @font-face rule has a unicode-range, the browser only downloads that file if the page uses at least one character in the range. Split a font into Latin, Cyrillic or Greek files and each visitor fetches only the scripts on the page.

Related: Core Web Vitals guide, PageSpeed Insights explained, Font pairing tool, CSS text styling.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap