Font fallbacks with (almost) no headache

29 August 2023
This article was originally published on Medium

Published On:

29 August 2023

Tags:

CSS
Google Fonts
Lighthouse
Page Speed
Web Development

If you, like me, are regularly running PageSpeed tests on your web pages, you probably are aware that Lighthouse is penalizing the use of render-blocking web fonts.

What does render-blocking mean?

Since the browser needs to download the web font, there’s a time span (which might vary based on several factors) where the text on the webpage is not shown. Text finally appears on the page once the font, and all of its needed variations, are loaded.

This behavior is called FOIT (Flash Of Invisible Text) and it’s discouraged by Google, because it’s considered bad UX. Luckily there’s a simple solution for that which is using the font-display descriptor.

Font Display

This CSS descriptor for the @font-face rule defines how font files are loaded and displayed by the browser. The default value for it is auto which tells the browser to use its default method, which normally equals the use of block. When enabled, the browser will render an invisible placeholder while loading the font, and then render the actual text once the font has been loaded.

If we want to avoid FOIT we can switch this property to swap. By doing this we are, in fact, telling the browser to render the text immediately, using the fallback font (which is available locally) and then replacing it with the final web font.

In 2019 Google Fonts added native support for this (you just need to append &display=swap to the import statement) and right now it appears to be the default when copy/pasting the font embed snippet from their dashboard.

Looks like we already solved the problem, right? Well, yes and no…

the secret to lifePEANUTS © PEANUTS Worldwide LLC

Goodbye FOIT, hello FOUT

Yes, we did solve the original issue. But by doing so, we introduced a new issue called FOUT (Flash Of Unstyled Text). We are now, in fact, rendering text from the very beginning and then replacing the font as soon as we’re ready. But what happens when we do the replacement?

An example of FOUTAn example of FOUT

In this example I’m using Lato (a Google Font) and Arial as a fallback. We can observe that, even though Arial seems to be a valid fallback font, there are some differences that are causing the line to break at a different point. Not only this is visually annoying but can also lead to other issues like CLS (Cumulative Layout Shift) which will drop our PageSpeed score.

The solution

Let’s start with the not-so-great news: There’s no perfect solution to this.

We cannot get around the fact that our web fonts need to be loaded and will not be available immediately. We can (and we should) do several things to make it happen faster:

  • Pre-connect to the font hosting domain

  • Preload the font stylesheet file asynchronously

  • Make sure to include only the font variations that we are actually going to use

    , in order to download the lightest possible CSS file.

However, no matter how fast we do this, the font will be loaded asynchronously, which means that the user visiting the page for the first time will see the font being replaced at some point.

Since we can’t avoid this, the best thing we can do is to make the replacement smoother.

Tweaking the fallback font

When I started researching the topic, I found out that a recommended approach was to create a set of CSS customizations specifically for the fallback font. However, this also requires the use of the CSS font loading API to detect if our final font was loaded and, based on that, append a CSS class to the body via Javascript. In this way we could apply some style customizations (font-size, line-height, letter-spacing etc.) to the fallback font and get rid of them once the final font was loaded. Unfortunately we can’t achieve this without JS since there is no way to target only the fallback font purely via CSS.

Luckily there’s an easier solution.

size-adjust

size-adjust is a CSS descriptor for the font-face rule which has been available on all major browsers (except, unfortunately, Safari) since mid-2021. It allows us to specify a percentage value that will determine how the glyph size will be adjusted.

Example:

@font-face { font-family: "custom defined fallback font"; src: local(Arial); size-adjust: 98%; }

This is very simple and at the same time very powerful. However it might be not enough to solve our problem since in most cases we will also need an extra layer of customization.

F-mods

F-mods is a short word for describing Font Metric Overrides. This is a series of CSS descriptors for the font-face rule that allows overriding the font metrics of a font.

Here’s a recap of the descriptors:

  • ascent-override customize the maximum distance that a glyph can have above the baseline

  • descent-override customize the maximum distance that a glyph can have below the baseline

  • line-gap-override customize the line-gap also known as leading

They have been supported in all major browsers (except, you guessed it, Safari) since the end of 2020 and, used in combination with size-adjust, allow a pretty good level of customization.

The solution in action

Let’s go back to our previous example, where we use Lato as a final font, and Arial as a fallback. If we overlap the two fonts, without applying any override, it will look like this:

Arial, the fallback (in grey) is overlapped with LatoArial, the fallback (in grey) is overlapped with Lato

Let’s now see how we can use the CSS descriptors we mentioned earlier. To do so we will create a new font face definition, called “fallback for Lato” (a name defined by us, it can be anything) which will use Arial as a local font. And let’s directly set size-adjust to 100%.

@font-face { font-family: "fallback for Lato"; src: local(Arial); size-adjust: 100%; }

After adding this definition, we also need to target this new font face in our font styling:

.myText { font-family: "Lato", "fallback for Lato"; }

Let’s see the result:

only size adjust

It looks already way better. We probably don’t need to touch the size at all, however we can observe that both the ascent and the descent could use some adjusting.

How do I know how to set these values?

When I first tried to solve the problem for a real client, my approach was basically trial and error: I was just tweaking the values in my IDE and checking the visual result until I got a satisfying result.

This process was quite tedious, therefore I decided to build an online tool that could help identify the correct parameters in a fast and efficient way.

The tool is called WHAT THE FOUT! and it supports all the available Google Fonts.

the interface of WHAT THE FOUT!the interface of WHAT THE FOUT!

Using the tool, I ended up with these values:

  • Ascent Override: 96%;

  • Descent Override: 20%;

Let’s now put everything together:

@font-face { font-family: "fallback for Lato"; src: local(Arial); size-adjust: 100%; ascent-override: 96%; descent-override: 20%; }

The overlapped fonts will now look like this:

FOUT comparison after fix

We can observe that the 2 fonts are now overlapping pretty well, also in regards to ascent and descent.

It’s now time to check how this looks like when loading the font:

FOUT after fix

As we can see, the swapping of the font feels way more natural now and, most importantly, we are not causing any layout shifts after the final font has been loaded.

Conclusion

As already mentioned, this solution is not 100% perfect. It’s about finding the compromise between a good user experience and the best possible page performance.

When adopting it we should also keep in mind what our requirement in terms of browser support is. As I mentioned earlier, this fix will not work in Safari. The other major browsers (Chrome, Edge, Firefox, and Opera) have been supporting it since mid-2021 which means that, if we need to support older versions as well, it’s probably better to go with the CSS + JS dynamic class solution explained earlier.

But if our goal is to have smooth font swapping with (most) modern browsers and make Lighthouse happy, this is definitely a painless and pretty effective solution.

Antonio Cosentino © 2021 - 2023 · All rights reserved