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.
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
- HTML
- DNS
- TLS
- CSS
- DNS
- TLS
- WOFF2
fonts.googleapis.com -> fonts.gstatic.com
Self-hosted
- HTML
- WOFF2
miguelpv.dev — connection already open
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');
}
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
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
- HTML
- parse
- WOFF2
- text
With preload
- HTML
- parse
- text
- WOFF2
<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
wofffallback to thesrclist, 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.