← Back
[W] Writing

A font is a performance decision

On October 30 I cut the font bytes on a typical HextaUI page from 126 KB to 48 KB, with pixel-identical rendering.

Before126 KB
After48 KB
Font bytes on a typical page, from the commit message. That's Inter plus Paper Mono.

The idea is simple. Your page uses a small fraction of the characters in a font, so ship two files.

Two files, one family

The first file holds what almost every page needs, and it's preloaded. The second holds the rest, and the browser only downloads it if a page needs one of those characters.

CSS does that part for you, using unicode-range. Type something below to see which file each character comes from.

Uses the real unicode ranges from HextaUI's Inter files. Try the samples, or type your own name.
Inter core36.5 KB
Inter extended29.1 KB
Paper Mono core11.4 KB
Paper Mono extended15.2 KB
The two core files, in red, add up to the 48 KB. The extended files only load when a page needs them.

The hard part was "pixel-identical"

Making a font smaller is easy. Making it smaller without changing anything took 411 lines of Python, because so many things can quietly change how text looks.

  • Accented letters have to stay with their base letters in the core. Kerning and mark placement don't cross font files, so splitting them would change the spacing.
  • The weight range starts at 400. Nothing on the site goes lighter, so that part can go. If a page ever does, the script has to be told first.
  • Optical size and the top of the weight range keep their full ranges, because clipping them changes the outlines.
  • Glyph names and the source bounding box stay. Chrome on macOS antialiases small text differently without them. That one's strange, but it's real.
  • The no-break space is always in the core, because the chart library measures it on every chart page.
  • Both files share one family name. Put a fallback font between them and it would draw the extended characters before the real font had loaded.

That list is why I'd call this design work. Every one of those rules exists to protect how the type looks. If you only care about bytes, you'd get a smaller file and a slightly worse site, and you probably wouldn't know why.

Don't trust yourself to remember

The script reads the built site to find out which characters are actually used. But content changes. A new symbol or a Turkish letter could quietly fall through to the extended file.

So there's a check, pnpm fonts:check, that fails when the committed fonts no longer match the site. I'd rather have a check fail than rely on remembering to rerun a script months from now.

It also has to read text that only appears after you interact, like toasts, dialogs and ⌘K. A scan of the rendered pages wouldn't see any of those.

What happens while it loads

Saving bytes helps, but there's still a moment before the font arrives. That's the part people actually see, so Inter Fallback in the CSS has overrides that make the system font take up the same space as Inter. When the real font swaps in, the text doesn't jump.

That's the whole job here: fewer bytes, and nobody can tell. You don't get to see any of this, and that's how you know it worked.

Next