Web Font Strategy:
the four steps I always run

Self-host, WOFF2, subset, preload — measured on this site.

Every character on this site is set in JetBrains Mono, so if the font is slow, the whole page is. The three weights I use come to 825 KB as the TTFs you'd download from the foundry. What actually ships is 65 KB, from my own server, requested before the browser even knows it needs it. This is how I get from one number to the other, in the order I do it.

Fonts Web Performance Core Web Vitals WPO

Self-host the font

My first move is always to take the font off any third-party host and serve it from my own origin. A Google Fonts link sends the browser to two servers it has never talked to: one for a stylesheet, which blocks rendering, and another for the font file that stylesheet points at. Each new server costs a DNS lookup and a TLS handshake before a single byte moves. Self-hosting folds all of that into one request on a connection the page already opened.

A preconnect hint can start those handshakes earlier, but it can't remove the stylesheet in the middle. And the old argument that visitors already have Google's copy cached stopped being true around 2020, when browsers began keeping a separate cache for every site.

Owning the file also means owning its cache headers. My font files never change under the same name, so the browser can keep them for a year without ever asking again:

/assets/fonts/*
  Cache-Control: public, max-age=31536000, immutable

Speed is why I do it, but it pays twice: one fewer outside dependency, and nobody else's server seeing who visits. That last part isn't hypothetical: in 2022 a German court ordered a site owner to pay a visitor damages for embedding Google Fonts, because it sent their IP address to Google without consent.

Google Fonts

  1. HTML
  2. DNS
  3. TLS
  4. CSS
  5. DNS
  6. TLS
  7. WOFF2

fonts.googleapis.com -> fonts.gstatic.com

Self-hosted

  1. HTML
  2. WOFF2

miguelpv.dev — connection already open

What the browser waits on before the first font byte arrives. Each box is one step it can't skip, not a measured duration: the third-party route adds two DNS lookups, two handshakes and a stylesheet before the font even starts downloading.

Ship it as woff2

Format is the free win. woff2 is the most compressed web-font format there is, and every browser I support has read it for years. Same glyphs, a third of the bytes: JetBrains Mono Regular is 274 KB as a TTF and 92 KB as WOFF2.

So I serve WOFF2 and nothing else, one @font-face per weight, each pointing at a single file. No format fallbacks, no list of sources for the browser to work through.

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  src: url('/assets/fonts/jetbrains-mono-v24-latin-regular.woff2')
       format('woff2');
}
The regular weight, as this site declares it. Medium and semibold are the same rule with a different weight and file.

Subset, and cut the weights you don't use

In proportion, this is the biggest cut: WOFF2 already took the files down to a third, and the subset divides them by more than four again. A full font file carries Cyrillic, Greek, Vietnamese and hundreds of symbols this site never renders, so I serve the Latin subset only. It covers the Spanish half of the site too, because á, é, ñ, ¿ and ¡ all live in Latin, so one file serves both languages. The full WOFF2 is 92 KB; the Latin one is 21 KB.

Then I cut the weights down to the ones the design really uses. Here that's three, regular, medium and semibold, with no italics: every extra weight or style is one more file to fetch.

  • TTF, full 825 KB
  • WOFF2, full 280 KB
  • WOFF2, Latin 65 KB
The three weights this site uses (400, 500, 600), added together. TTF and full WOFF2 are from the official JetBrains Mono 2.304 release; the Latin subset is the files this site actually serves. Almost 13× lighter, with nothing visible lost.

Preload the ones that matter

A browser doesn't request a font when it reads the @font-face. It waits until it has parsed the page, applied the styles and found text that actually uses that face. That's late, even with the CSS inlined like it is here. A preload in the <head> starts the download straight away, in parallel with everything else:

<link rel="preload" href="/assets/fonts/jetbrains-mono-v24-latin-regular.woff2"
      as="font" type="font/woff2" crossorigin />

Two details matter here. crossorigin isn't optional, even on your own domain: browsers always fetch fonts in CORS mode, so a preload without it doesn't match the real request and the font downloads twice. And I preload exactly the faces the first screen uses (here, all three) and no more. Preload everything and your fonts end up fighting your own critical assets for bandwidth.

Without preload

  1. HTML
  2. parse
  3. WOFF2
  4. text

With preload

  1. HTML
  2. parse
  3. text
  1. WOFF2
Without preload, the font request waits for parsing to finish. With it, the request leaves while the <head> is still being read, so the font is already there when the text needs it. Illustrative, not to scale.

When I'd do it differently

These four steps are my default, not a law: they're what this site needed. A few things would change my answer:

  • More fonts: the more families and weights a design leans on, the more subsetting and cutting matter, and the harder it gets to preload them all without choking the critical path. Past a point the real fix is fewer fonts, not cleverer loading.
  • A language outside the Latin script: if I ever add one, I'd ship its glyphs as a separate subset behind unicode-range, so English and Spanish readers never download them.
  • Genuinely old browsers: I ship WOFF2 only because every browser I build for can read it. If I had to reach much older ones, I'd add a woff fallback to the src list, and only those browsers would download the extra bytes.
  • A font I can't make fast: if a brand face had to render and I couldn't get it small, subset or preloaded, the honest move is to change the font, not to ship a slow one.

And the payoff isn't special to my site. On a site a friend and I recently reviewed, fixing the fonts alone (self-hosting, WOFF2, subsetting, fewer families and preloading the right ones) cut the font payload by roughly 10×. It took the page from a PageSpeed score of 47, with FCP and LCP around 10 seconds, to 92–96, with both between 2.0 and 2.7s. Same content, same design. The fonts just stopped being the bottleneck.

That's the whole strategy: make the font small and serve it early. Open the Network tab, find your font files and check their size and when they start. Those two numbers tell you which step to start with.

Related reading