Optimizing fonts for the web
Tags: webdev, frontend, performance
Published: 2021-11-10 17:33:43

> This post was originally published on my blog page at [try.catch.wtf](https://try.catch.wtf/blog/optimizing-fonts-for-the-web)

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\*](https://indestructibletype.com/Jost.html) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pug9jmgwwps62vvizo2l.png)

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](https://web.dev/variable-fonts/) has a great article on it.

![Jost variable font 700 weight vs actual 700 weight](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nn5a8a9rvodbife2yhgv.png)

> You can use [Variable Fonts](https://v-fonts.com/) 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 \*](https://indestructibletype.com/Jost.html) using [glyphhanger](https://github.com/zachleat/glyphhanger).

```shell
# 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
```

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](https://www.alphabet-type.com/tools/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.

```shell
# 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
```

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.

```css
@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;
}
```

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.

```css
@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;
}
```

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](https://css-tricks.com/fout-foit-foft/). Use `swap` only when the font is absolutely necessary. Make sure to deliver the font early enough to prevent layout shifts.

![Timeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w1blqs0bq6mabpobsaxb.png)

### `block`

Displays invisible text for a. Once the custom font downloads, it swaps the font. It can cause 'Flash of Invisible Text' or [FOIT](https://css-tricks.com/fout-foit-foft/).

![Success timeline 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8zyvz79nb6e2zj1nldr7.png)

![Success timeline 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ht2ib9b2fgrrx26etah8.png)

### `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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v7pnhddudggjqt4g0rpz.png)

![Success timeline 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wh4t7ilkthtpx6n6s5kk.png)

![Failure timeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jyqg67n8x05qr7mrot60.png)

### `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.

```html
<head>
  <link
    rel="preload"
    href="/fonts/Jost/Jost-400.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
</head>
```

But this comes at a cost 😔. `preload`ing resources might obstruct other critical resources for the page. If at all you end up `preload`ing 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.

```html
<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>
```

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 `preload`s. 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](https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements).

## 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! 👋😽