← Thin Margins

Self-hosting web fonts, properly

2026-08-21

Serving fonts from your own domain removes a DNS lookup and a TLS connection to a third party, and browsers no longer share cached fonts between sites anyway. Doing it well takes four steps.

1. Ship WOFF2 only

Every browser in use supports WOFF2, and it is about 30% smaller than WOFF. TTF and EOT fallbacks only add bytes to the stylesheet.

2. Subset

A full Latin font with every glyph can be 100 KB per weight. If the site is in English, keep Basic Latin, Latin-1 punctuation and the few symbols you use:

pyftsubset Inter.ttf --flavor=woff2 --output-file=inter-latin.woff2 \
  --unicodes="U+0000-00FF,U+2013-2014,U+2018-201D,U+2026,U+20AC" --layout-features='kern,liga'

Declare the same range in unicode-range, so a page that happens to contain other characters can still fall back to a second file instead of rendering boxes.

3. Decide what the reader sees while it loads

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

swap shows the fallback font immediately and replaces it when the web font arrives. For body text this is the right choice; the cost is a visible reflow, which size-adjust and ascent-override on the fallback face can make almost invisible.

4. Preload the one file the first screen needs

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

The crossorigin attribute is required even on the same origin, because fonts are fetched in CORS mode; without it the preloaded file is downloaded twice. Preload only what is used above the fold. Every preload competes with the HTML and CSS for the first round trips.

A variable font, as in the example above, replaces several weight files with one, which usually makes the total smaller once more than two weights are in use.