Font fallbacks with (almost) no headache
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…
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?
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:
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 baselinedescent-override
customize the maximum distance that a glyph can have below the baselineline-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:
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%.
After adding this definition, we also need to target this new font face in our font styling:
Let’s see the result:
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.
Using the tool, I ended up with these values:
Ascent Override: 96%;
Descent Override: 20%;
Let’s now put everything together:
The overlapped fonts will now look like this:
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:
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.