Jason Thorsness

github
github icon
linkedin
linkedin icon
twitter
twitter icon
hi
27Jun 03 25

Font Around and Find Out

I produced a 580-byte monospace font that renders all code points in Unicode planes 0, 1, and 15 as empty glyphs. Can we go smaller? The specification says yes, but Google’s reference WOFF2 decoder has denied me my ultimate victory with a haughty error: “implausible compression ratio”.

Here is some text rendered in my font. Copy/paste or un-apply the font to view.

(╯°□°)╯︵🙃
← it's there

How and why did I do this? What did I learn about fonts and font optimization along the way? Keep reading to find out!

Background

Text on the web can be displayed in any font. However, there are no standard fonts available everywhere. Site authors typically provide a list of fonts, ending in a CSS-standardized generic family. Usually this is okay. But what if it isn’t?

If a site author insists a specific font should be used, they can distribute it as a resource along with the site itself using CSS @font-face. Now the site will render the same on all platforms — but the problem is not completely solved.

Font resources are usually delivered as separate files. What if the file download is very slow? The site author has a few options. The most conservative option available today is font-display: block, which means the browser will wait about three seconds for the font to download before falling back to something else. This is a well-thought-out idea designed to make web sites usable on poor connections. It’s typically great. But in some cases, it’s the wrong behavior.

Consider what would happen if web browsers used fallbacks for other resources. What if Chrome generated AI fallbacks from alt-text with Gemini for slow-loading images? Why stop there — maybe Safari could use an LLM to hallucinate the HTML itself from the requested URL. Clearly this would be unacceptable for images and HTML and sometimes it’s the same for fonts.

When using an exact font is just as critical as using the right CSS and the site is broken without it, the site author needs the web browser to block indefinitely or fail rather than fall back. This is when font-display start to feel inadequate. There is no block-forever option; after three seconds the browser will show the text in whatever way it wants. Unless we fight back.

Font Tell Me What I Can And Can’t Do

I sought a way to defeat the bullying browser behavior and implement block-forever. My requirements were:

  1. Cross-platform: Chrome, Safari, and Firefox.
  2. No JavaScript required.
  3. No measurable performance impact.

I determined that without JavaScript, the way to succeed would be by working within the fallback system itself. As it turns out, a font can be included inline in the CSS using a data URI, thus being instantly available. Problem solved? Not quite: while this covers requirements 1 and 2, web fonts can be tens to hundreds of kilobytes which can slow the download of CSS and hurt performance. However, if a font is small enough it’s okay. So the plan became:

  1. Create a tiny font of empty glyphs and embed it inline in my CSS.
  2. Use this tiny font as a fallback for my critical font served the typical way.

Easier said than done. How to make a font that blocks fallback? How to make it extremely tiny? To go further, I was forced to understand what a font actually is.

What the Font?

Text is represented in computers as a sequence of numbers called Unicode code points. A modern font is a single-file database containing instructions on how to transform these sequences of code points into graphics on the screen. The most important contents of a font are:

  1. A table of glyphs: tiny drawings of letters, numbers, and symbols.
  2. A table mapping Unicode code points to corresponding glyphs.

To display text using a font you can look up the glyph for each code point in your text and draw it on the screen. Then beyond that simple concept there are dozens of other table types that make text actually look good in practical situations.

So at first look, it seems a font that maps all code points to a single empty glyph might be quite small and effective. Luckily at this early stage I was unaware and thus undaunted by the struggle that lay ahead.

Vibe Fonting

There’s an excellent Python library called fonttools that can do most anything with fonts, and as it turns out ChatGPT is pretty good at using it. So to get started on my tiny font I went through a series of iterations with the LLM. Here is the LLM-assisted progression as a text-based montage:

How can we make a character render as a blank space? Can we just map it to the .notdef glyph? No! It falls back to the system font. What about a glyph with no contours? Nope, that falls back too. How about a glyph with a single degenerate contour? A first small success! An empty region is shown.

Is there an efficient way to map all code points to that glyph? CMAP table format 13 is perfect. But Claude and ChatGPT both claim it is not universally supported (editor’s note: Google Fonts uses format 13 for a fallback, so maybe it’s OK but I am not confident).

How about format 4? This compact format can only map a few thousand code points to the empty glyph before its length limit of 64KiB is exceeded — it’s optimized for mapping contiguous ranges of code points to contiguous glyph ids, and any deviation requires extra space.

Format 12 is not limited by a length limit, but is also designed for contiguous range mapping and the font gets huge when I map all code points to one glyph.

At this point the vibe was broken and I pondered — am I stuck? Then I remembered it isn’t the uncompressed size of the font that matters, but the compressed size: and WOFF2 fonts are compressed with Brotli.

Bro(tli) Do You Even Lift

Brotli is a compression algorithm optimized for the web and in fact fonts in particular. It effectively finds patterns and reduces any repeating sequence of bytes to a very small representation. The uncompressed size of the tables of cmaps and glyphs don’t matter — we can make them as large as necessary as long as they have a simple repeating structure Brotli can recognize and compress to a small size.

In fact, things we add to a font can counterintuitively end up making it smaller if they make it more suitable for compression.

Toward this end, rather than attempting to map each code point to a single empty glyph, we can add ~65535 identical empty glyphs. This will allow us to map ranges of code points to ranges of glyphs, which the cmap format 12 was designed to do, reducing its size to roughly a single entry per plane. Then Brotli will be able to compress the enormous repeating sequence of glyphs to almost nothing.

This worked. The resulting font is only 336 bytes. But my victory was short-lived.

Punished for Excellence

The resulting font doesn’t work in Chrome. And probably anywhere else. In a fatal lapse of confidence, the reference WOFF2 decoder deems any compression ratio over 100:1 to be “implausible” and simply gives up. As it appears in the reference implementation, this shocking lack of faith in the power of Brotli has leaked everywhere and is unlikely to ever change (though I filed a bug report anyway.)

error
play stupid games, win stupid prizes?

Settling for Second-Best

I can adjust the number of glyphs in the font to get a compression ratio just under 100:1, passing the check. This results in a font that is 580 bytes, which is still quite small. In fact, it’s so small that this makes little difference to the end result of this project — except in my sad knowledge of what could have been. Here’s the complete font, which could be roughly half the size.

d09GMgABAAAAAAJEAAoAAAABimAAAAH2AAEAxQAAAAAAAAAAAAAAAAAAAAAAAAAABmAAhAgKhPEAgr1aATYCJAPOFAvOFAAEIAUGByAb9cgArgpsY9ZIP9Bg7gzmplVTDYXYIQNyy1+Nfz6OXs/Ntr0sILE/0eCfD5KtoG0EAt4NMOAB7Qqa8U4pgF0hgHF2mxuAusy0DP1nIbk5DemFurz8cF1+sQEiCHfnR5CESLNGDWo3aWvUZgS08y+KvPOyslKMA5nQeLPNNtuy4ZI3/2uB4Pl2gj54Jt8xL7HEotiSne9uQOOR5fBxOOTpDaeWUx5IHI8LNoSXYMQm1lZi4aPd0LFLfrEmmOYUYU1c+J/zXoAvjJc+4b/xzV/oLyBoc2ocegjjnDWIdfbggAPQD+ACcJZHOCuwUCAslCynEZYzvFoRXm3cV4T7xsOB8HDy9kB4e/JYER4bTw/C08vXJISvKTzvIjzv8VID4aUWf3+Ev12qdkSqDnnfEXk/FBhEFBhSTAZRTE41w4hqRtRtQNRtVO5GlCua2xHNZZVfROVPmxlJLNq6ULx10aE46PtD5379QwyMrLJ/jt6MZS+MxfEzfifKRJ1qptppY9qcPqbPGWFGnBJTcuaZeafCVJwT5sR5Yp7azHqEsyIs1PeuyvsCaz3wInsAyGfQAoCQmewBMCCNaQCAeCa+p3z783qJvEapw4/bLcHPhrsJgC3WgPA74bDWyEqYWVXOvUv0S/YAuLLyW8NZ0RoBwU1brAP+b0kmAA==

What Else?

Some approaches I learned while making this empty font smaller also apply to reducing the size of web fonts in general. In particular, reordering glyphs to align with contiguous ranges of characters and adding placeholder glyphs to fill small gaps in mapped ranges can reduce font sizes without affecting the rendered text.

I’m sure some (most?) font pipelines already do this, but not all — for example I can reduce the size of Cascadia Mono by at least 5% by applying these techniques. This is the font I am ultimately planning to use and it’s enormous so every little bit helps.

Thanks for reading! For more articles, subscribe to my RSS Feed or follow me on X.

 Top 
TermsPrivacy