Quire Ink
/find

Letterforms, and the making of pages

What a font subsetter actually removes

A webfont is mostly glyphs your pages will never show — alphabets you do not write in, symbols nobody has typed since 1994. Cutting them is the largest single saving available to a text site, and the reader gives up nothing at all for it.

A retail text font carries two to three thousand glyphs. A blog in English uses perhaps two hundred of them; add Vietnamese and it is four hundred. Everything else is Greek, Cyrillic, currency for markets you do not serve, and arrows.

Subsetting removes what a page cannot show. It is not compression and it is not a quality trade: the glyphs that stay are byte-identical to the ones the foundry drew.

Three separate cuts, and they compound

The saving is easy to predict before you run anything. For a face of G glyphs of which a site can show g, and a per-glyph cost that is roughly constant within a family,

saved1gG

Two hundred glyphs kept out of twenty-eight hundred is a saving of about ninety-three per cent, before a single variation axis has been touched.

Cut one is the codepoints. Keep latin, latin-ext and vietnamese, drop the rest.

Cut two is the variation axes. A variable font stores deltas for every axis it declares across its whole declared range. A family offering weights 400 to 700 in the product is storing 200-to-900 deltas nobody can select.

Cut three is the tables: hinting instructions no modern renderer consults, and layout features a text page never triggers.

pyftsubset Literata.ttf \
  --unicodes-file=vietnamese.txt \
  --layout-features='kern,liga,onum,tnum' \
  --variations='wght=400:700' \
  --drop-tables+=DSIG \
  --flavor=woff2 --output-file=literata-vietnamese.woff2

It prints back what it kept, and this block is the other kind — a fence with no language on it, which is what most fences are. Nothing here knows a grammar to colour it with, so only the two things true of any notation are marked: what sits inside quotes, and a $NAME.

kept   "latin, latin-ext, vietnamese"   dropped  greek, cyrillic, +19 more
axes   "wght 400..700"                  dropped  opsz, ital
wrote  $OUT/literata-vietnamese.woff2   46.2 KB  (was 97.6 KB)

What the cuts are worth

Measured on this site's own faces, from upstream sources rather than by re-cutting an already-cut file:

Cut Family Before After
Codepoints Literata 187 KB 41 KB
Axis clamp Source Sans 3 35.6 KB 28.5 KB
Both Inter, 3 subsets 46.1 KB 38.5 KB
Both IBM Plex Mono, 6 subsets 71.2 KB 65.6 KB

The part that surprises people

Declaring a face is not downloading it. Split a family into subsets and give each a unicode-range, and the browser fetches only the ranges a page's text actually lands in. An English post never pulls the Vietnamese file even though the CSS names it, and a post with no code never pulls the mono face at all.

Which means the honest number for "how much font does a reader download" is not the size of the family. It is the size of the ranges that page happens to touch — and that is a number you can only get by measuring a real page rather than a directory listing.

@font-face {
  font-family: 'Literata';
  src: url('/fonts/literata-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
  font-display: swap;
}

Tip

Re-subset from the foundry's original every time, never from the file you shipped last. Feeding a subsetter its own output re-rolls the compression, so every run produces a diff of about thirty bytes about nothing.