DEV Community

Ahmed Nader
Ahmed Nader

Posted on AI-assisted

Google Fonts Hands You a 1.5 MB Font. Your Page Needs 26 KB of It

Go to Google Fonts, pick a family, Download it, and drop the file in your project. For Noto Sans Display a fine specimen of mundane webfont — that file is 1,540,036 bytes. One and a half million bytes of outlines, for a landing page that renders about ninety distinct characters.

As usual, the advice is "use font-display and move along" But file size is not vibes, it's arithmetic, so I did the arithmetic: opened the font file, counted what's in it, whittled it down, and stress-tested the outcome. These are the numbers.

The census

The file you downloaded is the variable font: one file carrying every weight from Thin to Black plus a width axis — that's why it's 1.5 MB instead of the 350 KB a single static weight costs. Inside, both versions tell the same story:

Variable download Static Regular
Glyphs 3,316 3,316
Simple outlines (drawn directly) 1,796 1,796
Composites (references to other glyphs) 1,487 1,487
Empty slots (structural, no ink) 33 33
Bytes 1,540,036 357,864
Character map 5,680 codepoints 5,680

Note the 1,487. Nearly half the glyphs in this font are composites, a concept the "bag of shapes" mental model can't express and will find out in a minute.

Meanwhile, your landing page renders roughly ninety distinct characters.
English copy, some punctuation, digits. The rest is ink for Cyrillic, Greek, five styles of typographic quotation mark, and mathematical operators your marketing copy has never met.

The subset, measured

The keep-list for a typical English site: Basic Latin (U+0020–007E), Latin-1 Supplement (U+00A0–00FF, so accented characters in borrowed words don't boxout), the General Punctuation block for real quotes and dashes, plus € and the true minus sign. Now the interesting fork in the road:

Subset the variable file, keep it variable. The subsetter carves the variation data down to the surviving glyphs along with the outlines. What comes back is still a genuine variable font — both axes, every weight from 100 to 900, every width — for 349 glyphs:

Path Glyphs Bytes % of download
The download, as-is 3,316 1,540,036 100%
Landing-page subset, still variable 349 135,048 8.8%
Instanced to Regular, then subset 349 26,148 1.7%

That second row is a legitimately great option nobody talks about: all nine weights of exactly the characters your page uses, in 135 KB. Bold headlines, light captions, one file, full design freedom.

If you ship one weight — say, Regular and let the browser synthesize nothing
— pin the axes first (instancing, 1.5 MB → 356 KB, which is the static font regrown from the VF) and then subset: 26,148 bytes, byte-for-byte the same result as subsetting the static file. That's a 98.3% cut of the file you actually downloaded. One time, zero runtime cost.

Both outputs are complete, valid fonts. Not amputated ones — and the reason 349 glyphs come back when you asked for ~90 characters is the whole trap below.

The é test

Here's the mistake that gives subsetting its bad reputation.

In this font, é is not drawn. It's a composite — a tiny record saying "take e, take the combining acute accent, stack them here." Remember: 1,487 of our 3,316 glyphs are these references.

Now suppose your keep-list contains é but not e — say you're shipping a French page and decide a bare "e" isn't on it. A naive subsetter deletes the e glyph obediently. What happens to é? It's still in the font, still pointing at a glyph that no longer exists. Your page renders a box where é should be.
The character you kept breaks because of the character you removed.

A correct subsetter walks the reference chain before deleting anything. I tested this on the same font: requested exactly one character, U+00E9, nothing else. What came back from the static file:

  • 5 glyphs, 1,456 bytes
  • the é composite itself
  • the outline of e — the same 2 contours, 32 points I counted in the original
  • the acute accent it references
  • two bookkeeping glyphs (.notdef and friends)
  • and a character map that maps only U+00E9

It passes on the variable font too: 5 glyphs, 5,100 bytes, still variable on both axes, é still a live composite over its surviving components.

That last line is the subtle part, and it's correct behavior: e's ink lives in the file (é needs it to render), but e is no longer typeable — it's not in the character map, so no keyboard can reach it. The subsetter kept the drawing and dropped the identity. If your tool instead hands you a font where é renders as a box, it doesn't walk composites, and no amount of "but I kept é!" will save the file.

This test is one line to run and it's the single best quality check for any subsetting tool. If é survives the deletion of e, the tool understands fonts.
If it doesn't, don't ship that output.

What I got wrong on the way

My first verification of that é subset was broken, and it's worth admitting how. I checked whether the output still contained a glyph named e. It didn't — names get renumbered or dropped during subsetting — so for a moment I "proved" that the closure had failed and e was gone. The e outline was sitting right there as glyph00001, identical to the original; I was checking identity by name in a file format that doesn't promise names. The honest test compares outlines and character maps, not labels. Font tooling will happily rename everything under you; test what a glyph is, not what it's called.

The counterfindings

Three things the headline numbers don't tell you.

Hinting wasn't part of the win. Older fonts carry hinting — pixel-grid instructions that can occupy a real slice of the file. Noto Sans Display ships with none at all (0 bytes of hinting in both the static and the variable builds; modern Google Fonts have dropped it), so there was nothing to strip. On an older hinted font it can matter; on modern ones there's nothing there.

Subsetting keeps what you keep, including variability — and that cuts both ways. The 135 KB variable subset preserves every weight and width of the surviving glyphs, which is a feature. But if your tool collapses variation tables without telling you — and many GUI tools do — you silently shipped one frozen weight while believing you shipped a VF. Run the subset and check for an fvar table before you celebrate the file size.

You can't subset your way out of emoji. Color emoji fonts embed bitmaps per glyph and are measured in megabytes; the pictures are the point. The 1.7% figure is for outline fonts doing normal text work.

And one thing that isn't about file size at all: subsetting creates a modified font, and licenses have opinions. The SIL Open Font License permits modification, but many OFL fonts declare Reserved Font Names
your subset must not ship under the original's name. "I only removed characters" is not a defense any EULA has accepted. Thirty seconds with the license file beats the email from the foundry.

Run it yourself

All the numbers above are one command each, using fontTools:

pip install fonttools

# the census
python3 -c "from fontTools.ttLib import TTFont; print(len(TTFont('Font.ttf').getGlyphOrder()))"

# the landing-page subset of the VARIABLE font: 1,540,036 -> 135,048 bytes, still variable
pyftsubset "NotoSansDisplay[wdth,wght].ttf" --output-file=subset_vf.ttf \
  --unicodes="U+0020-007E,U+00A0-00FF,U+2000-206F,U+20AC,U+2212"

# or pin the axes first, then subset: -> 26,148 bytes
fonttools varLib.instancer "NotoSansDisplay[wdth,wght].ttf" wght=400 wdth=100 -o regular.ttf
pyftsubset regular.ttf --output-file=subset_regular.ttf \
  --unicodes="U+0020-007E,U+00A0-00FF,U+2000-206F,U+20AC,U+2212"

# the é test: request ONE character, check what survives
pyftsubset regular.ttf --output-file=eonly.ttf --unicodes="U+00E9"
Enter fullscreen mode Exit fullscreen mode

Then open eonly.ttf and render é. If it draws, the tool walks composites.
If it's a box, delete the tool from your pipeline.

The rules worth keeping

  • Subset the deliverable — the web embed, the app bundle, the client handoff. Your working library stays full-coverage, or you'll discover mid-project that you need Ł in a headline.
  • Decide which deal you want: 135 KB for every weight of your characters (still variable), or 26 KB for one pinned weight. Both beat 1.5 MB.
  • Run the é test on any tool before you trust it — and check whether your VF came out the other side still variable.
  • Check the license for reserved font names; rename your subset if needed.

Full disclosure: I build a mobile font engineering tool —
Font Studio's subsetter runs this same closure-preserving subset on-device, which is how I ended up counting glyphs on a phone at midnight in the first place. Whatever tool you use, the numbers above are the bar: a 1.5 MB download, a 26 KB job.

Your users won't notice. Your page weight will.


Ever shipped a subset that rendered boxes where é should be? Or found a font
in production carrying scripts your app will never render? I'd love to hear the story in the comments.


Top comments (0)