The real cost of “full” fonts

A complete webfont file carries every stylistic variant and glyph the designer created, even when a page needs only a subset. Those files can easily run to multiple megabytes. The @font-face rule exists precisely to avoid that waste: it lets you split a font family into separate resources — unicode subsets, distinct weights, and other variants — so the browser fetches only what the render tree actually requires.

That lazyloading behavior is efficient, but it has a hidden drawback. The browser cannot know which font files it needs until it has constructed the render tree from the DOM and CSSOM. Font requests therefore fire late in the critical rendering path, after other resources have already been dispatched. If the font has not arrived by first paint, the browser may render the layout with no text at all — the “blank text” problem.

Font critical rendering path

Two platform features fix this: <link rel="preload"> for fetching fonts early, and the CSS font-display property for controlling what renders while a font is still loading.

Preload fonts you know you need

When a page predictably needs a particular webfont from a known URL, add it to the critical path with <link rel="preload">. The browser will start the font request immediately, before the CSSOM exists, rather than waiting for render-tree construction to reveal the dependency.

Define your own invisible-text policy with font-display

Preloading raises the odds that a font is ready at first paint but guarantees nothing. For the gap cases, font-display lets you choose how the browser draws text whose font-family is not yet available. It splits a font download into three periods:

  1. Font block period: Text using the font is rendered with an invisible fallback. If the font loads before the period ends, text is painted with it.
  2. Font swap period: Text falls back to a visible system font. If the webfont loads during this window, it swaps in.
  3. Font failure period: A font still missing when this period starts is marked failed, and fallback rendering becomes permanent.

Set the behavior you want by adding the property to @font-face rules:

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 400;
  font-display: auto; /* or block, swap, fallback, optional */
  src: local('Awesome Font'),
       url('/fonts/awesome-l.woff2') format('woff2'), /* will be preloaded */
       url('/fonts/awesome-l.woff') format('woff'),
       url('/fonts/awesome-l.ttf') format('truetype'),
       url('/fonts/awesome-l.eot') format('embedded-opentype');
  unicode-range: U+000-5FF; /* Latin glyphs */
}

Supported values are auto, block, swap, fallback, and optional.

Scripted control via the Font Loading API

For cases where preload and font-display are not enough, the Font Loading API exposes font faces to JavaScript. You can construct a FontFace, tell the browser to fetch it immediately, and track its status programmatically:

var font = new FontFace("Awesome Font", "url(/fonts/awesome.woff2)", {
  style: 'normal', unicodeRange: 'U+000-5FF', weight: '400'
});

// don't wait for the render tree, initiate an immediate fetch!
font.load().then(function() {
  // apply the font (which may re-render text and cause a page reflow)
  // after the font has finished downloading
  document.fonts.add(font);
  document.body.style.fontFamily = "Awesome Font, serif";

  // OR... by default the content is hidden,
  // and it's rendered after the font is available
  var content = document.getElementById("content");
  content.style.visibility = "visible";

  // OR... apply your own render strategy here...
});

Scripting adds overhead, but it buys flexibility that CSS alone cannot provide. You can hold all text rendering until the font is ready, implement a per-font timeout, or render with a fallback and swap styles once check() confirms the webfont is available. These strategies can be mixed within a single page, applying different treatments to different sections.

Cache webfonts like the static resources they are

Font files change rarely. They deserve a long max-age and a conditional revalidation token such as an ETag so repeat visits do not re-download them. A service worker serving fonts cache-first also works well for most applications.

Keep fonts out of localStorage and IndexedDB. Both have performance tradeoffs that make them inferior to the browser HTTP cache as the delivery path.

Enforcement checklist

  • Override default lazyloading: use <link rel="preload">, font-display, or the Font Loading API to keep text visible and prevent layout shifts while fonts download.
  • Set far-future caching with revalidation: fonts are static and infrequently updated; serve them with a long-lived max-age and an ETag. Service workers should use a cache-first policy.

Lighthouse can police these rules in CI. The relevant audits are Preload key requests, Uses inefficient cache policy on static assets, and All text remains visible during WebFont loads.