Get your free SEO audit today — Call 91 060 30 90
Home / Blog / Web Design
Web Design

Web fonts and performance: font-display, WOFF2, subsets and preloading so text appears instantly

You open a website and, for a moment, the headlines aren't there. Or they are, but in a typeface that isn't the brand's, and half a second later everything shifts into place. That's the web font effect: the browser has to download them before it can paint text with them, and how you handle that wait decides whether the text appears instantly or the page moves while you read.

We'll go from what happens in the browser to what you can change today in your CSS and HTML: font-display, WOFF2, subsets, preloading, an adjusted fallback font and how to measure it all.

How fonts affect LCP and CLS

The browser doesn't download a font just because you declared it in @font-face: it waits until there is text on the page that uses it. The download starts late, after the CSS has been fetched and processed. While the file is on its way, the browser has to decide what to do with the text, and there are two classic behaviours:

  • FOIT (flash of invisible text): the text is hidden until the font is ready. If it takes long, the user sees blank gaps.
  • FOUT (flash of unstyled text): the text is shown straight away in a fallback font and swapped for the web font when it arrives. It is readable from the start, but the change is noticeable.

LCP measures when the largest content element in the viewport is painted. Often it's an image, but many times it's a headline or a big paragraph. If that text is hidden while waiting for the font, LCP can't happen until the font arrives. Google considers an LCP of 2.5 seconds or less to be good, measured at the 75th percentile of real visits.

CLS measures unexpected content movement. If the fallback font is wider or taller than the final one, when it is swapped the lines break differently and blocks grow or shrink, pushing down whatever is below. A good CLS is 0.1 or less.

There is no free setting: hiding the text avoids the jump but delays LCP; showing it sooner speeds up LCP but risks CLS. The way out is twofold: get the font there sooner (format, size, preloading) and make the swap barely noticeable (an adjusted fallback).

font-display: what each value does

The font-display property, inside @font-face, tells the browser how long to wait and what to show. It works with two periods: the block period (invisible text) and the swap period (text in the fallback, which is swapped if the font arrives). Exact timings are decided by the browsers; those in the table are approximate.

ValueBlockSwapWhen to use it
autoUp to the browserUp to the browserBetter not to leave it as the default
blockAbout 3 sInfiniteIcon fonts
swapAlmost noneInfiniteGeneral choice, with an adjusted fallback
fallbackAbout 100 msAbout 3 sCompromise if a late swap worries you
optionalAbout 100 msNoneSecondary fonts or zero CLS

My rule of thumb: swap with an adjusted fallback for almost everything. fallback if you worry about the swap happening when the user is already reading. optional if zero CLS is the priority and you accept that on the first visit the fallback may be what people see (with preloading it has a better chance of arriving in time). block, only for icon fonts. And don't leave auto: its behaviour depends on the browser.

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

WOFF2, variable fonts and subsets

Today one format is enough: WOFF2. It compresses with Brotli, weighs noticeably less than TTF or OTF and is supported by every current browser (check the compatibility table if you have to support something very old). EOT was for old versions of Internet Explorer, and TTF, OTF or WOFF1 only add weight and lines of CSS. A single URL in src is enough. Since WOFF2 is already compressed, don't expect extra savings from also serving it with gzip; what does matter is that the server delivers it with the right type (font/woff2).

A static font is one file per weight and style: regular, bold, italic and bold italic are four downloads. A variable font stores a continuous range in a single file (for example, weights from 100 to 900). It isn't always lighter: with many axes it can exceed a static file. It usually pays off when you use three or more weights of the same family; with just regular and bold, two subsetted static files may win. Measure both options.

Subsetting is the biggest lever. A complete font includes thousands of glyphs (Cyrillic, Greek, Vietnamese, symbols) and your Spanish website uses a few hundred. With fonttools you can generate a file containing only what's needed: basic Latin, the Latin-1 supplement (accents, ñ, ¿, ¡), typographic punctuation and the euro sign.

pip install fonttools brotli
pyftsubset Inter.ttf --unicodes="U+0020-007E,U+00A0-00FF,U+2013-2014,U+2018-201D,U+2026,U+20AC" --layout-features="*" --flavor=woff2 --output-file=inter-latin.woff2

Check the licence before modifying a font: some require you to change the name when distributing a modified version. And check your content: if you write in Catalan or publish names with uncommon characters, a missing glyph will be painted in the fallback font.

With several languages you can split the font into subsets and declare each one with unicode-range: the browser downloads only the ones the page needs. On a Spanish-only site, a single Latin file is usually the simplest option.

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A0-00FF, U+2013-2014, U+2018-201D, U+2026, U+20AC;
}
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0100-017F;
}

Preloading, self-hosting and Google Fonts

Because the browser discovers the font late, you can tip it off from the HTML with a preload:

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

It comes with several traps:

  • The crossorigin attribute is mandatory, even if the font is on your own domain. Fonts are requested in anonymous CORS mode; without the attribute, the preload isn't reused and the file is downloaded twice.
  • Preload only the critical ones: one or two, those on the first screen. Every preload competes with the CSS and the LCP image.
  • The URL must match the one in @font-face exactly.
  • Don't preload subsets that may not be used: preloading downloads them regardless. Chrome warns in the console if you preload something that isn't used.

A preload doesn't fix a poor setup. Declare the @font-face in the main CSS (or in inlined critical CSS), avoid @import chains and don't depend on a script that loads fonts late: every link in the chain delays the moment the browser knows what it has to download.

If you use Google Fonts, the page requests the CSS from one domain and the files from another (fonts.googleapis.com and fonts.gstatic.com): two new connections, and a CSS file that blocks rendering before the font is discovered. Self-hosted, it goes over the same connection, you control the subset and you can preload it. The old argument that "it will already be in the visitor's cache" no longer holds: the main browsers separate the cache by site, so a download made by another website doesn't benefit you.

Self-hosting also gives you control over caching. With versioned file names (for example, inter-latin-v4.woff2) you can afford a year:

<FilesMatch "\.woff2$">
  Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

Privacy weighs just as much. Loading Google Fonts from Google's servers means the visitor's IP address reaches Google. In 2022, a regional court in Munich ruled that loading them this way without consent was unlawful and awarded a visitor damages. It's a first-instance German ruling, not binding in Spain, but it stoked the debate; self-hosting avoids the problem at its root. Check it with a legal adviser. Also check the licence of each family: Google Fonts families usually allow you to download and host the files, but it's worth verifying.

If you stay with Google Fonts: request only the weights you use, keep display=swap and add preconnect for both domains.

A fallback font that doesn't move the page

The jump appears because the fallback and the final font have different metrics: average character width, height above and below the baseline, and their own line spacing. CSS lets you create a "disguised" fallback from a local font and adjust it so it takes up almost the same space:

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

size-adjust scales the glyphs to match the average width; ascent-override and descent-override set the height above and below the baseline, and line-gap-override the intrinsic line spacing, all as a percentage of the font size. The values in the example are indicative: work out those for your font with a tool such as Capsize or Fontaine, or with the automatic generation of some frameworks (Next.js includes it). Keep in mind that local() depends on that font existing on the device and that support for these descriptors isn't identical in every browser: check MDN or Can I use. Where they aren't supported, the site works just the same; only the small jump comes back.

To check the result, block the web font from DevTools (in the Network tab, the file's context menu, "Block request URL") and reload: you'll see the page with the fallback only. Compare it with the final version. If the headline takes the same number of lines and nothing moves, the adjustment is well tuned.

The radical alternative is the system font stack: zero downloads, instant text and no jump, because you use the device's own typeface.

body {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

The trade-off is that the site looks different on each system and your brand loses personality. A common middle ground: the system stack for long text and a brand font only for headlines, with a single file.

How many fonts to use and what to do about icons

As a rule, one family and at most two; and of each, two or three weights (or one variable font). Every family, weight and style is one more request and one more risk of a jump. Before adding a font, ask yourself whether the same design can be achieved with another weight of the one you already have. And if the italic only appears in two words, it may not deserve a file of its own.

Icons are similar. An icon font downloads hundreds of glyphs even if you use eight; until it arrives you'll see gaps or odd characters; screen readers may read out meaningless symbols, and it only supports one colour. SVG icons, inline or in a sprite, with only the icons you use, weigh less, look sharp at any size and are more accessible (aria-hidden="true" on decorative ones, alternative text on those that convey something).

How to measure and a final checklist

Don't trust your gut: measure before and after.

  • DevTools, Network tab: filter by "Font". See how many files there are, how much they weigh and when they start in the waterfall: if they start late, they're discovered late. Disable the cache and throttle the connection (for example, "Slow 4G") to see the worst case.
  • DevTools, Performance tab: record a reload and find the LCP marker and the layout shifts. In the Rendering panel, the "Layout Shift Regions" option highlights whatever moves.
  • PageSpeed Insights: compare field data (real visits) with lab data. The former is aggregated over a window of roughly four weeks, so it takes a while to reflect a change.
  • WebPageTest: the filmstrip and the waterfall show when the text appears. Repeat each test several times and compare with and without your changes.

Final checklist:

  • One or two families, with two or three weights (or one variable font).
  • WOFF2 only, self-hosted and subsetted for your languages.
  • font-display declared in every @font-face.
  • One or two critical fonts preloaded, with crossorigin and the identical URL.
  • Fallback font with size-adjust and the three overrides, or a system stack.
  • Long cache lifetime and versioned file names.
  • Icons in SVG, not in a font.
  • LCP and CLS measured before and after, in the lab and in the field.

Frequently asked questions

Is font-display: swap always the best option?

It's the safest place to start, because the text is visible from the first moment. But without an adjusted fallback it can cause layout shifts. If CLS worries you more than seeing the brand font on the first visit, try optional with preloading. It's a design decision as much as a technical one, so it's worth agreeing it with whoever looks after the brand.

Should I preload all my fonts?

No. Preload only those used on the first screen, normally one or two. Preloading too much competes with the CSS and the main image, and can worsen LCP instead of improving it.

I already use Google Fonts and it works fine, is self-hosting worth it?

Usually yes: you save external connections, control caching and subsets, and avoid sending visitor data to a third party. It's a small change on most websites: you download the files, subset them, change the URLs in the CSS and remove the connections to Google's domains. For the legal side, check with an adviser.

More on Web Design

Shall we talk about web design for your business?

Tell us about your project and we'll tell you how we can help, no strings attached.

Call 91 060 30 90