Improving web font performance: a case study

On the 8th and 9th of November 2018, I had the chance to join the first edition of the performance.now() conference, focusing on today’s most important web performance insights. Among the sixteen great talks, I took a great deal away from Zach Leatherman’s presentation on Web Font Performance and I would encourage you to watch the full talk.

At Netcentric, where I am a Front-end Software Engineer and web performance guardian, I could immediately see areas in which I could begin to try out some of the tips mentioned to improve web font implementation. I’ll illustrate a few points of analysis that could be useful for optimizing web font performance.

Insights for optimizing web font performance

Setup

I mainly used macOS 10 with Chrome 70 for my analysis, and always throttling the network to Fast 3G and CPU 4x slowdown.

Similar fonts

Let’s start by simply loading the homepage of the website to see which files are being loaded:

We are loading 5 font files, the .ttf and the CAC-Regular cannot be touched as they are very specific but I noticed that we have 3 fonts with quite a similar name:

I wondered how different they really are and if we really needed all of them, so I decided to compare them visually by placing one font on top of another. I chose to use the CS-Light font as a base font because it seemed easier to match with the other fonts. In our case, setting a lighter font-weight to the CS-Regular font didn’t make any difference, so I could only apply a bolder font-weight.

So, I have the CS-Regular font in green, the one I want to replace, and the CS-Light in red and they don’t really match.

We obviously have 2 different fonts

The first word here looked acceptable, but then it gets pretty wild. However, I could see some similarities so I tried to get a closer match by playing a little with some CSS:

@font-face {
  font-family: "CS-Light";
  src: url("CS-Light.woff2") format("woff2");
}

@font-face {
  font-family: "CS-Regular";
  src: url("CS-Regular.woff2") format("woff2");
}

.cs-light, .cs-regular {
  font-size: 2em;
}

.cs-regular {
  color: green;
  font-family: CS-Regular;
}

.cs-light {
  color: red;
  font-family: CS-Light;
  letter-spacing: 0.012em;
  line-height: 1.345em;
}

Adding a letter-spacing and a line-height to .cs-light

By playing with two CSS properties, letter-spacing and line-height, which are both well supported, I could actually get a better match:

This isn’t perfect, but I was surprised at how close I could get and how similar those two fonts are.

I also wanted to make sure they still match even if I change the font-size:

Font-size: 1em

Font-size: 3em

I really liked the result. You can feel a slight difference, especially when the text is made a bit larger, but overall it looks similar.

Then, I wanted to also check the other font we have, called CS-Demi, to see if I could get similar results:

CS-Light cs CS-Demi

Again, it didn’t really match at the first place. So, I went through the same process and played again with some CSS properties, also adding a font-weight property since this font was much bolder than the other one:

@font-face {
  font-family: "CS-Light";
  src: url("CS-Light.woff2") format("woff2");
}

@font-face {
  font-family: "CS-Demi";
  src: url("CS-Demi.woff2") format("woff2");
}

.cs-light, .cs-demi {
  font-size: 2em;
}

.cs-demi {
  color: green;
  font-family: CS-Demi;
}

.cs-light {
  color: red;
  font-family: CS-Light;
  font-weight: 600;
  letter-spacing: 0.031em;
  line-height: 1.37em;
}

Also adding a font-weight property

In the end, I got a pretty decent result. It’s not as good as the previous results, but it’s still hard to tell the difference between these two fonts if you don’t know exactly which is which:

Font-size: 2em

So, now we can get a pretty similar result by loading only one file instead of three. Let’s see what the gain could be:

344.7 KB of web fonts

We are loading 344.7 KB of web fonts separated in 5 file requests.

After getting rid of CS-Regular and CS-Demi, we now have only 197.7 KB of web fonts among 3 file requests, so that’s a gain of 147.KB, which represents ~42.6% of the total weight of the fonts of the page. Not bad.

FOIT and FOUT

Another problem we’ve faced with websites is called Flash Of Invisible Text (FOIT) and also Flash Of Unstyled Text (FOUT). You can definitely see it visually when the network is a bit slower than usual. The Lighthouse audit has also been complaining about it:

The loading of web fonts are timing out (3,000ms)

Thanks to the font-display property, we can avoid a FOIT pretty easily, using the fallback value:

@font-face {
  font-family: "CS-Light";
  src: url("CS-Light.woff2") format("woff2");
  font-display: fallback;
}

Font-display: fallback

As explained on the Google Developers’ website:

fallback gives the font face an extremely small block period (100ms or less is recommended in most cases) and a short swap period (three seconds is recommended in most cases). In other words, the font face is rendered with a fallback at first if it’s not loaded, but the font is swapped as soon as it loads. However, if too much time passes, the fallback will be used for the rest of the page’s lifetime. fallback is a good candidate for things like body text where you’d like the user to start reading as soon as possible and don’t want to disturb their experience by shifting text around as a new font loads in.

Be careful though: font-display is not fully supported yet!

Finally, after adding this property, we can fix our FOIT, at least on browsers supporting this feature:

FOIT is not a problem anymore

However, our FOUT problem remained, so to fix this issue I took a look at the preload hint.

Preload

We need to prioritize our CS-Light request

By examining the timing of the request of our CS-Light web font, I could see that it took almost 5 seconds to load, and it’s also being loaded at the same time as the other fonts:

Before preload

So I decided to preload the critical web font by simply adding 1 line on the head section (see preload specifications):

<head>
  <link rel="preload" href="CS-Light.woff2" as="font" type="font/woff2" crossorigin="anonymous">
</head>

Preload the main font file

I could then see straight away that Chrome was loading the main font file before the other ones, and faster too:

Our critical font file has now a high priority

Time to download the resource is divided by 2

Therefore, we started to download our critical web font resource after 1.45s instead of 5.33s, resulting in a gain of 72.8%. This meant it took 2.14s instead of 4.29s to download, meaning a gain of 50% regarding the length of time taken to download the content.

After all those optimizations, we could finally put everything together. We ended up with something that looked like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Improving Web Font Performance: A Case Study</title>
    <link rel="preload" href="CS-Light.woff2" as="font" type="font/woff2" crossorigin="anonymous">
    <style>
      @font-face {
        font-family: "CS-Light";
        src: url("CS-Light.woff2") format("woff2");
        font-display: fallback;
      }
      .cs-demi, .cs-light, .cs-regular {
        font-family: CS-Light;
        font-size: 2em;
      }
      .cs-demi {
        font-weight: 600;
        letter-spacing: 0.031em;
        line-height: 1.37em;
      }
      .cs-regular {
        letter-spacing: 0.012em;
        line-height: 1.345em;
      }
    </style>
  </head>
  <body>
    <p class="cs-light">CS-Light</p>
    <p class="cs-demi">CS-Demi</p>
    <p class="cs-regular">CS-Regular</p>
  </body>
</html>

Next steps

There is still room for improvement regarding the optimization of the web fonts, and I‘ll continue to share the rest of the analysis. The next area to explore will be the possibility of subsetting fonts, since the website is being delivered in many languages.

Feel free to share your experience regarding web fonts optimization below or get in touch if you have any questions about these insights.