Font optimization to make an ultra lightweight website

Weeks ago, I discovered the 512 KB Club, a group of websites focused on performance with a very small footprint. Browsing the site, I learned about the three teams (green, blue, and orange) into which websites are split based on their size. I wanted this blog to fit in the tiniest group of all: The Green Team (for websites with less than 100 KB).

Analyzing the size of all the elements that make up the home page, I saw that it was right at the limit of the green team. Just publishing a new article would cause it to exceed the limit. Then, I noticed that the two fonts that I use were responsible for 60% of the bytes transferred.

That was the reason I decided to optimize both files to include only the glyphs that would be used.

There are many tools to solve this problem, both online and offline. I prefer to use offline tools because they are not usually dependent on a third party to host them or change their usage policy. If you feel the same way, jump straight to the offline section.

Online API: Google Fonts§

If somebody does not have a working Python installation, which is rare nowadays (even on Windows systems), they could use the Google Fonts API to solve this problem. I am not a fan of using Google's services, but I include them just for completeness.

This API can generate WOFF2 files (which is the preferred format for displaying fonts online). These files can be optimized by splitting them into subsets, such as Latin, Cyrillic, Greek, etc., and also by specifying individual Unicode characters.

Language subsets§

To query the API, we must use its base URL: https://fonts.googleapis.com/css?, specify a font family: family=Inconsolata and, optionally, its weight: :700.

curl "https://fonts.googleapis.com/css?family=Inconsolata:700"

Accessing the above URL will return a list of CSS “font-face” at-rules containing each subset of languages supported by the font:

/* vietnamese */
@font-face {
  font-family: 'Inconsolata';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  src: url(https://fonts.gstatic.com/s/inconsolata/v32/QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp2I7WRL2l2eY.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
...
/* latin */
@font-face {
  font-family: 'Inconsolata';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  src: url(https://fonts.gstatic.com/s/inconsolata/v32/QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp2I7WR32lw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

We can download the subsets we need or embed the previous code directly into our site's style sheets. This approach would not be sufficient in my case, as it barely saves any bytes compared to the fonts I already use.

Individual glyphs§

If we need finer-grained control over the glyphs that will be part of our font, the API allows us to specify a list of UTF-8 characters. We should add &text= and the list of characters to the URL used in the previous section. UTF-8 characters should be escaped with the % symbol and, of course, byte by byte. For example, to generate an Inconsolata font variant for only the following text: ¡Hola! we should create this query.

curl "https://fonts.googleapis.com/css?family=Inconsolata:700&text=%c2%a1Hola!"

These two approaches give you a coarse and a fine filter to create your own optimized font. The only problem is that it uses a Google service, and that may be a no-go for you.

The good-ol' local tools§

There are tools to inspect and create custom fonts that contain only the glyphs you need. We will be using two Python libraries which are very easy to use: FontTools (compress, merge, filter and convert formats) and FreeType-py.

The cleanest way to install them is to create a virtual Python environment, which we can easily delete after this process.

python -m venv fonts # Create virtual environment
cd fonts
source bin/activate.fish # Activate python venv (I use fish shell)
pip install fonttools[ufo,lxml,woff,unicode] # Install FontTools with the required dependencies
pip install freetype-py # Install Freetype

After downloading the desired font (in my case, Inconsolata)1 in TTF format, we can examine each glyph contained in the file with the following script:

python -c 'import freetype, sys;
stdout = open(1, mode="w", encoding="utf8");
face = freetype.Face(sys.argv[1]);
stdout.write("".join(sorted([chr(c) for c, g in face.get_chars() if c]) + ["\n"]))' \
  Inconsolata/static/Inconsolata-Regular.ttf

2 It outputs:

List of all the glyphs contained in the Inconsolata font

As we can see, there are many glyphs that could be omitted simply because they are used to display basic graphics in a terminal/console. There are also variations of common glyphs that can be omitted because they do not appear in Latin-derived languages or in English.

I have created two .txt files with a list of glyphs for both the bold and regular variants. The bold variant is only used in titles, so it contains even fewer glyphs.

FontTools allows us to create an optimized font file by specifying the above character list and also output the result in a proper format for web development.

A quick overview of the parameters:

  • --text-file = <file>: a list of the glyphs to be included in the font, specified in a plain text format.
  • --desubroutinize: "Remove CFF use of subroutines". Although enabling this may be counterintuitive, it will produce smaller files for small subsets.
  • --no-hinting: Hinting is useful when the font is viewed on a very low resolution screen, but it takes up space. This is very rare nowadays, so hinting can be omitted.
  • --flavor = <format>: Format of the output font.
    • <format> is set to woff2, a newer version of the woff format that improves compression.
  • --output-file=<file>: where the tool saves the results.
fonttools subset --text-file=glyphs-bold.txt Inconsolata/static/Inconsolata-Bold.ttf \
    --desubroutinize \
    --no-hinting \
    --flavor=woff2 \
    --output-file=inconsolata-bold-light.woff2
    
fonttools subset --text-file=glyphs-reduced.txt Inconsolata/static/Inconsolata-Regular.ttf \
    --desubroutinize \
    --no-hinting \
    --flavor=woff2 \
    --output-file=inconsolata-regular-light.woff2

After running the tool, the resulting files are ready for its use on our site. We can observe a size reduction factor of almost 10 times the original size!

.rw-rw-r-- 100Ki Inconsolata-Bold.ttf
.rw-r--r--  11Ki inconsolata-bold-light.woff2

.rw-rw-r--  99Ki Inconsolata-Regular.ttf
.rw-r--r--  11Ki inconsolata-regular-light.woff2

Inspecting the glyphs of the font file§

You may be interested in inspecting the contents of the generated files.

We could use the freetype-py library as in the previous snippet to print its contents, but there is a small problem. This tool does not work with WOFF2 files, so we need to convert them to another format, such as WOFF, and then print their contents.

We should run the following statements in an interactive Python shell or script:

from fontTools.ttLib import TTFont
f = TTFont('inconsolata-bold-light.woff2')
f.flavor='woff'
f.save('inconsolata-bold-light.woff')
f = TTFont('inconsolata-regular-light.woff2')
f.flavor='woff'
f.save('inconsolata-regular-light.woff')

Both the bold and regular variants of the font are now in the WOFF format.

.rw-r--r--  14Ki inconsolata-bold-light.woff
.rw-r--r--  15Ki inconsolata-regular-light.woff

We could inspect every glyph that the fonts contain just by changing the input file name to the previous freetype-py snippet.

3 The output for the bold variant:

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~ ¡¢£¤¥¦§¨©ª«¬­®¯°±´µ·¸»¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿĂıŒœʻʼˆ˚–—‘’‚“”„•…′″‹›⁄€™↑↓−∕�

3 And finally, the output for the regular variant:

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿĂıŒœŸʻʼˆ˚˜    ‐‒–—‘’‚“”„•…′″‹›⁄€™←↑→↓∂∅∆∈∏∑−∕⊕○●✓✕�

Of course, this optimization can be combined with others recommended by performance analyzers such as PageSpeed Insights, GTMetrix, WebPageTest, or Pingdom, to name a few.

One of the most important is to enable caching of files that do not change frequently, such as JS, CSS, and font files. Both NGINX and Apache explain in their documentation how to tell clients which files to cache and how to set a reasonable expiration time.


I hope this article has helped you improve the size and performance of your website. It is really important to keep focusing on these efficiency issues. We live in a world where it is now common to have gigabit connections at home, and yet large web portals are tens of megabytes, are bloated, and take seconds to load. Both users and developers should demand websites that load almost instantly.

Also, as this year is almost over, I wish you a Happy New Year 2024!


1

Use this link to download a compressed archive with all the font variants.

2

I used an image instead of text because the font I use (which is already optimized) does not contain the glyphs displayed and would force the browser to use a fallback font (which would be at least ironic).

3

Notice that this is a piece of text, unlike before, and you can confirm that only the Inconsolata font is being used. Open developer mode <F12> key -> Inspector -> Fonts.