Optimizing Webfonts

2025-10-20

Fonts are a critical part of web performance: Custom fonts can cause significant layout shifts, and their file size can be quite enormous. The fastest option is to bypass them completely by using web-safe fonts. However, a custom font can provide a lot of personality to a website, so many web designers consider them indispensable. So how can we make a font as small as possible to reduce its impact on web performance?

Typefaces

Let’s use Fixel, the font I’m using on this website, as an example. Fixel comes in three variants:

Both the text and display typeface come in 9 weights in both italic and regular. To get the nomenclature right, the Fixel Display typeface consists of 18 fonts (like FixelDisplay-Bold.otf). The variable font, on the other hand, covers the full range in a single font. This sounds like a great improvement, but it comes at a cost: The variable font has a much larger file size. Each of the fonts of the display and text typeface is around 120-140 kB. The variable font is 470 kB.

Compression

The first thing we can do is compress the font. We do this with the Web Open Font Format: WOFF21 is basically an OTF or TTF font file compressed with Brotli. Let’s compare the sizes:

Font Uncompressed Size Compressed Size
Fixel Text Medium 138 kB 74 kB
Fixel Variable 472 kB 199 kB

Compression makes a much bigger difference for the variable font. So now we can say that if you use three or more fonts of this typeface, you should use the variable font instead. This is only the beginning of our optimization, though.

Subsetting

On a conceptual level, a font file maps every character to a glyph. A font might contain a lot of characters that don’t actually appear on your website. Fixel, for example, contains both Latin and Cyrillic characters. This is an amazing feature if your page is available in both English and Ukrainian. But it is also an opportunity to reduce it to the characters you need on your specific website from the 600 symbols that Fixel offers in every font. That process is called subsetting.

We will use the Python-based fonttools for that. If you have Python and pip installed, you can install it like this:

pip install fonttools brotli uharfbuzz

Alternatively, I also prepared a Dockerfile.

We identify the characters we need by specifying code points in Unicode or ranges thereof. This website, for example, features texts in English and German. Let’s start with the 94 (printable) ASCII characters within the range 0000 to 007F. This covers all letters used in the English language, as well as some common punctuation like . and mathematical symbols like +. For the German language, we also require the three umlauts in upper and lower case, as well as the lower case Eszett2:

00C4,00D6,00DC,00DF,00E4,00F6,00FC

Without going all typography nerd, we need to talk about a few other characters:

At this point, you might ask yourself what happens if you forget a character: Will it not render? With font-family: Fixel, system-ui, for each character not found in the first font the browser will fall back to the ones later in the list. This can lead to some quite quirky typography though:

A sample from this website where the Fixel font is missing the German letters

Let’s use fonttools to subset our two fonts above to the characters we’ve identified. We pass it the characters we want to keep, tell it to keep all “layout features” (we’ll get back to that later), and to compress it to WOFF2:

compress-fonts fonttools subset\
  --unicodes="0000-007F,00C4,00D6,00DC,00DF,00E4,00F6,00FC,201C,201D,201E,201C,00D7,00F7,2013,2014,2019,2026"\
  --layout-features="*"\
  --flavor=woff2\
  FixelText-Medium.otf

This results in significant savings:

Font Uncompressed Size Compressed Size Subsetted
Fixel Text Medium 138 kB 74 kB 29 kB
Fixel Variable 472 kB 199 kB 74 kB
What about user generated content? The subset above will work well if your page features German and English text plus the most important typography-nerd glyphs. You will run into problems when your page accepts user-generated content. This is even true if your site is completely in English, and people can leave comments in English: if a user with the last name Krüger leaves a comment, the umlaut will not be rendered in your font of choice. An alternative is to use the full Latin-1 range: `0000-00FF`. As explained in the Wikipedia article, this will cover a wide range of languages.

Typograhic Features

Modern fonts support a long list of typographic features. This goes way beyond the scope of this article. fonttools can strip down the features and comes with a default list of features it will keep that works quite well. It will, for example, keep ligatures, which browsers use by default if the font provides them (as is the case for Fixel). Previously, we told fonttools to keep all features with the --layout-features="*" option. When we drop that option, we get another drastic reduction in the file sizes:

Font Uncompressed Size Compressed Size Subsetted Reduced Featureset
Fixel Text Medium 138 kB 74 kB 29 kB 11 kB
Fixel Variable 472 kB 199 kB 74 kB 31 kB

Conclusion

In this article we saw how we can drastically reduce the file size of custom web fonts. We also saw the difference between a variable and non-variable font. In our case, the variable font is about three times the size of the non-variable font. We should therefore only use the variable font if we use more than two variants (combinations of font weight and italic/non-italic).

Thanks

Thanks to Marius and FND for their feedback ❤️

Footnotes

  1. WOFF and WOFF2 basically have the same level of support, so at this point you can deliver WOFF2 without any fallbacks

  2. There is also an uppercase Eszett, but we don’t need that unless we set German text in all-caps, as there is no German word that starts with an Eszett. 

  3. You are allowed to use it even when you are not a large language model.