Performance

Fonts: preload, swap and CLS, measured

Without font-display LCP is 644 ms; with swap it drops to 280 ms and CLS stays at 0.0028. Preload made FCP worse, from 280 to 456 ms.

Key points

  • Without font-display the text waits for the file: LCP 644 ms against 280 ms with swap.
  • CLS does not change between no font-display, swap and preload: 0.0028 in all three.
  • font-display: optional takes CLS to 0 and gives up the font when it arrives late.
  • Preload made FCP 176 ms worse because the font competed with the CSS for bandwidth.

Without font-display the text with a web font stayed invisible until an LCP of 644 ms; with swap LCP is 280 ms at the same CLS of 0.0028. In our test preload made FCP worse, from 280 to 456 ms, because the font took bandwidth from the CSS.

The four variants

VariantFCPLCPCLSLoad
No font-display280 ms644 ms0.0028626 ms
font-display: swap280 ms280 ms0.0028626 ms
swap + preload456 ms456 ms0.0028606 ms
font-display: optional284 ms284 ms0629 ms

The 364 ms between 644 and 280 is the block period: the browser hides the text until the file arrives, and LCP waits for that text. With swap the text appears at once in the fallback font and the measured CLS stays identical.

The CSS

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

html {
  font-family: "Inter", system-ui, sans-serif;
}

Serve the file from your own origin with a long cache and immutable: the one on our site answers with max-age=31536000 and weighs 48,256 bytes. Preloading the weight used above the fold needs crossorigin, otherwise the browser downloads the file twice.

Why preload made FCP worse

Preload started the font download earlier but also pushed the CSS back: on a 1.6 Mbps link two 48 KB files compete, and first paint went from 280 to 456 ms. Preload pays off when the font really is in the first screen and the CSS is already small; it is not a universal rule.

The rules that hold

  • Always set font-display: swap or optional: without it the text stays invisible for the block period. Source: web.dev, CC BY 4.0.
  • Two weights per family at most. Regular alone is 48,256 bytes in woff2. Source: MDN, CC BY-SA 2.5.
  • Preload only the weight used above the fold, with crossorigin. Source: MDN, CC BY-SA 2.5.
  • Close the gap with size-adjust and ascent-override on the local fallback, not with an arbitrary line-height.

The limitation to name

The 0.0028 CLS is small because Times and Inter have close metrics on this text. A wider fallback produces a bigger jump, and a large heading moves far more than a paragraph. The test uses one font and one page: the choice between swap and optional depends on what the typeface is worth, not only on the number.

Where to continue

The EneUI catalogue indexes 111,985 components, for instance gooey-text-morphing, and the grid of 12,808 icons such as icon-abc. Read font-display and CLS, the preload strategy, what causes CLS and how to set a performance budget.

Frequently asked questions

Is font-display: swap always the right call?

It is the default for most sites: text appears at once and the font follows. For logos and brand headlines, optional is worth considering.

Why can preload make things worse?

Because it competes with the resources that block rendering. In the test FCP went from 280 to 456 ms with preload on.

How do I reduce the font's CLS?

Use a local fallback whose metrics resemble the web font, tuned with size-adjust, ascent-override and descent-override. A fixed line-height is not enough.

How many font weights do I need?

Two in most cases, regular and bold. Every weight is a 40-50 KB file and on mobile that is bandwidth you do not get back.

Self-host or Google Fonts?

Self-hosting removes a third-party host, a handshake and a privacy question. In exchange you update the files yourself when the font changes.