DEV Community

Cover image for Optimizing fonts for the web
Potato Chip
Potato Chip

Posted on

Optimizing fonts for the web

This post was originally published on my blog page at try.catch.wtf

Adding fonts to your site can make it look better. And they come with some side effects leading to degraded performance. In this post, I will share how to use fonts without taking away a huge bite of your lighthouse scores 😤.

But before we jump into it, let's analyze some of these side effects.

  • Delayed text rendering

If a web font has not yet loaded, it can delay text rendering. Resulting in delayed First Contentful Paint (FCP) or even delayed Largest Contentful Paint (LCP) in some cases.

  • Layout shifts

When fonts are loaded and swapped, they can cause layout shifts. These layout shifts occur when a web font and its fallback font take up different amounts of space on the page.

What can we do to fix these issues 🤔?

Getting the font ready to serve

Before we go ahead with anything, let's see how we can optimally serve our font files as fast as possible. If you are using a 3rd party font provider (like Google Fonts), you cannot do much in this case. However, if you are serving the fonts yourself, make sure to serve them (or any static assets) over a CDN and HTTP/2.

A small font file will always be early to the party 🥳. Downloading multiple font styles (eg. weight, slant) will also hinder the UX. Wouldn't it be great if we could get away without downloading these other styles of the font and use only 1 font to rule them all 😬?

Synthetic weights

SHH 🤫! Designers hate this simple trick 🤬.

Instead of downloading multiple weights of the same font, we can load a regular variant (400 weight) and rely on the browser to synthetically create other weights. If the browser can create synthetic variants, then why do designers hate this trick? It seems to be working perfectly fine, right?

In the image below, the red text is the bold variant (500 weight) of Jost* font, and the blue text is created synthetically from the regular variant (400 weight). The difference might look subtle, but they can impair the UI and UX.

Jost font synthetic 700 weight vs actual 700 weight

If this doesn't bother you (or your designers), you can go ahead and use synthetic styles.

Variable fonts

A variable font has multiple styles like weight, width, slant, optical size, and italics (called axis) of the fonts. The font creator can create several axes, reducing the number of styles you need to download. For this case, we will use the weight axis. If you want to learn more about the other axes or variable fonts, web.dev has a great article on it.

Jost variable font 700 weight vs actual 700 weight

You can use Variable Fonts for finding and trying variable fonts

Font subsetting

Font subsetting is the process of taking a font file and reducing the number of characters (or character sets). For example, let's say you have a font with Japanese characters like 月. You are serving a page that is in English. It is unlikely that you will render Japanese characters. So we can remove them from our font and make a profit!

Let's subset Jost-400-Book.ttf (88.7 kb) from Jost * using glyphhanger.

# install glyphhanger
npm i -g glyphhanger

# install fonttools
pip install fonttools

# subsetting Jost-400-Book.ttf font to Latin charset
glyphhanger --LATIN --subset=Jost-400-Book.ttf
Enter fullscreen mode Exit fullscreen mode

The above script results in a Jost-400-Book-subset.ttf (40.5 kb) file. Already a reduction of 54% 😱!

There are online tools like Charset checker to check the charset of fonts. You can use this to verify if the subsetted font matches your required charset or not.

WOFF2 compression

WOFF2 is a compressed font format that can compress a TTF font.

Let's use our subset font Jost-400-Book-subset.ttf (40.5 kb) file and compress it to woff2.

# clone woff2
git clone --recursive https://github.com/google/woff2.git

# enter into the cloned repo
cd woff2

# build
make clean all

# convert the font
./woff2_compress ~/Jost-400-Book-subset.ttf
Enter fullscreen mode Exit fullscreen mode

This results in a Jost-400-Book-subset.woff2 (15.3 kb) file. A whopping decrease of 62% compared to the subset font (Jost-400-Book-subset.ttf) and an 82% decrease compared to the original font (Jost-400-Book.ttf) 😱!

If you are using glyphhanger for font subsetting, you can also compress to woff2 directly in a single command!

glyphhanger --LATIN --subset=Jost-400-Book.ttf --formats=woff2

Loading the fonts

Now that we have reduced the font file size, let's see how we can load the font.

@font-face {
  font-family: Jost;
  font-weight: 400;
  font-display: swap;
  font-style: normal;
  src: url("/fonts/Jost/Jost-400.woff2") format("woff2");
}

.custom-font {
  font-family: Jost;
}
Enter fullscreen mode Exit fullscreen mode

This specifies that a font located at /fonts/Jost/Jost-400.woff2, of woff2 type, with 400 weight, and normal style is referenced as Jost. Now we can use our font anywhere by setting font-family: Jost;. Browsers download fonts only if a styling on the page references them. In this case, the browser will only download the Jost font if the page has an element with class .custom-font.

The @font-face rule changes a bit when loading variable fonts.

@font-face {
  font-family: Jost VF;
  font-weight: 300 800; /* specify weight range of the font */
  font-display: swap;
  font-style: normal;
  src: url("/fonts/Jost/Jost-VF.woff2") format("woff2 supports variations"), url("/fonts/Jost/Jost-VF.woff2")
      format("woff2-variations");
}

.custom-font {
  font-family: Jost VF;
}
Enter fullscreen mode Exit fullscreen mode

The font-display property tells the browser when to render the font once it is loaded. It accepts the following values:

auto

This is browser default.

swap

The fallback font will be used immediately. Once the custom font downloads, it swaps the font. It can cause 'Flash of Unstyled Text' or FOUT. Use swap only when the font is absolutely necessary. Make sure to deliver the font early enough to prevent layout shifts.

Timeline

block

Displays invisible text for a. Once the custom font downloads, it swaps the font. It can cause 'Flash of Invisible Text' or FOIT.

Success timeline 1

Success timeline 2

fallback

Blocks rendering for a short time (100ms). If the font is still not downloaded, use the fallback font. Gives a swap period of about 3 seconds for the custom font to load. If it doesn't load within the swap period, it will not be used.

Success timeline 1

Success timeline 2

Failure timeline

optional

Like fallback, it blocks for a while and displays the fallback if the custom font is not yet downloaded. But, the browser decides whether to swap the downloaded custom font depending on the connection speed.

Preloading?

Sometimes, the absence of fonts can make your page unusable. It might be a no-brainer to load them asap because you know you WILL use them. We can preload fonts by adding a <link> in the <head> with preload hint. This way, instead of the font to be discovered via stylesheet and downloaded, the browser will download the font at the earliest.

Preloaded resources are cached on the browsers for future requests.

<head>
  <link
    rel="preload"
    href="/fonts/Jost/Jost-400.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
</head>
Enter fullscreen mode Exit fullscreen mode

But this comes at a cost 😔. preloading resources might obstruct other critical resources for the page. If at all you end up preloading fonts, make sure your longest critical chain is short.

When loading fonts from a 3rd party origin, you can preconnect to establish an early connection. Below is an example with Google Fonts.

<head>
  <!-- preconnect origin serving stylesheets -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />

  <!-- preconnect origin serving fonts -->
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

  <!-- loading stylesheets -->
  <link
    href="https://fonts.googleapis.com/css2?family=Jost&display=swap"
    rel="stylesheet"
  />
</head>
Enter fullscreen mode Exit fullscreen mode

Note that links with prefetch and preconnect hints are executed as the browser sees fit. Whereas preload is mandatory. Modern web browsers already have good prioritization and do not need preloads. But if you want some critical resource to be downloaded at the earliest, you can use it sparingly.

preconnect for fonts needs a crossorigin attribute because, unlike stylesheets, font files are served over CORS connection.

Conclusion

How does all this fix the 2 issues we discussed in the beginning?

  • Faster text rendering

A font will render faster if it is downloaded faster. Use variable fonts when needing multiple font styles. Subset and convert fonts to woff2 for the smallest font file size. Serve them over a CDN and HTTP/2 protocol for the fastest and most reliable speeds. preload fonts when serving them or preconnect fonts/stylesheets when using 3rd party services. When preloading, make sure your longest critical chain is the smallest possible

  • Layout shifts

If the custom font is not a top priority, use font-display: optional. This guarantees no layout shifts. But if the custom font is a top priority, use font-display: swap with the above optimizations for the font delivery.

Hope you find this post helpful! SA-YO-NA-RA! 👋😽

Top comments (1)

Collapse
 
starver20 profile image
Starver

Very detailed article.