We all get that woff2 is smaller than woff, or ditching custom fonts comes with best performance. But what are considerations if you really want to use custom fonts?
First of all, try to avoid large JavaScript libraries for your font loading strategy. Despite seeing it way too often when I'm doing pagespeed audits, WebFontLoader shouldn't be used anymore. Maybe, it shouldn't have been used back in the days, but let's say the alternatives and strategies are way better today.
Let's explore other font loading strategies and related techniques.
Preloading web fonts
If you want to preload, be sure to only preload fonts used above the fold, and a maximum of two. Because, when everything is important, nothing is. And preloading too much resources then actually turns out to be negatively impacting your FCP and LCP metrics.
As a result, I often try to determine which one or maybe two fonts are used above the fold across different template pages. Those might be worth to preload, others aren't.
Preloading Google woff files
I've seen sites that just went to the Google Font CSS files, and started to copy those URL's to prefetch them on their website. But Google will actually serve different CSS to different browsers, based on their user agent. So doing this isn't the best idea.
Preloading the wrong font files
If you do choose to preload fonts, it might sound obvious to preload the correct files. However, I've seen sites, especially on Laravel, where the linked woff files were given a query string containing a hash, while the preloaded fonts didn't had such query string. For example, the preloaded font file would be:
<link rel="preload" href="Roboto-Regular.woff2" as="font" type="font/woff2" crossorigin>
And then the stylesheet would contain:
@font-face {
font-family: Roboto;
src: url(Roboto-Regular.woff2?hj43) format("woff2");
}
These are considered different resources, as their file path isn't the same. As a result, the same file is being downloaded twice and both browsers and users aren't benefitting from your font preloading strategy.
Font face order matters
This could happen when a build tool or build process is being used which is adding query strings to static files. But also note that browsers will pick the first font typein your font-face declaration that they support.
So, when you would first declare the woff file, and then the woff2 file, more modern browsers would first start to downloaded the preloaded version, but then ending up downloading the first declared woff file. Again without any benefits.
Most browsers will warn you though when a preloaded file goes unused within the very first seconds. For example:
So be sure to keep an eye on the DevTools' JS Console.
What about icon fonts?
Your next questions might be, what about preloading icon fonts? I guess in the end it's a matter of preference. The way I like to look at it is as following:
Try to imagine which parts of the page is most important when just visiting a webpage for the first time. When it comes to engagement:
- the logo might be important because maybe it's a well known and trustworthy brand;
- Additionally, the blog title or product title is important as a confirmation that the user reached the page they were expecting when a link was clicked via a shared post or Google results;
- And when talking about product or listing pages, the product image is important. Obviously, it has to be downloaded, but that doesn't make it less important.
However, within the first unique pageview, icons aren't as important yet. I won't be looking at the cart or favorites icon right away. You do want to preserve the space of icons to prevent layout shifts. But icons themselves aren't as important towards user engagement.
The CSS @import solution
When using Laravel, or specific Wordpress plugins, you might have seen this construction: Google Fonts, or other fonts being embedded by starting your main stylesheet with an @import:
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
And when it comes to Adobe typekit fonts, this is the default implementation.
@import results in a performance penalty
This implementation doesn't make any sense, as you aren't speeding things up. Although you're embedding the stylesheet referring to some fonts, the fonts themselves still have to be downloaded.
So, instead of improving font flickering or layout shifts, there are only losers: your visitors. We've seen FCP improvements of 33%. We looked at the 80th percentile of mobile and uncached pageviews. So, don't use this.
Don't use typekit. It requires you to do an @import, which does another @import. And sometimes, typekit is then using an additional @import. We saw the following in a case where we removed this construction:
@import will regress unique pageview FCP by 48.3%
This is caused by browser behaviour: a browser will only start to download the referenced @import file once the main CSS file containing the @import is fully downloaded.
So, you're basically creating a chained request and that's why @import should be prevented at all times. Or lazyload the CSS containing such @import construction.
@import use case
Let's share some use-case outcomes to show the benefits of removing @import. Petfoot Discounts removed an @import and their mobile FCP dropped from 2.5 to 1.2 seconds at the 75th percentile. However, this is a simplified conclusion as public Core Web Vitals data isn't telling us the impact of TTFB nor differences per cached or uncached page.
For this, we could use another case where a double chained @import construction was used, as they were loading fonts via Typekit.
- Vipio.nl saw their unique pageview FCP improving from 2780ms to 1872ms after removing the double @import construction. This is a 32.7% improvement;
- When we get rid of TTFB fluctuations and just look at the pure TTFB to FCP differences before and after, wins are 41%.
- Or if you want to turn it into a clickbait article by calculating and communicating in reversed order: your unique pageview FCP will regress by 69.5% when using the typical typekit construction.
Self hosting fonts
Even better than lazyloading, or actually even better in general, is self-hosting fonts.Since late 2020, Chrome followed Safari's example by partitioning HTTP cache.
This basically means that if two websites are both using the same Google Font, served by the same Google Server, a browser will still end up downloading it twice. This wasn't the case before, but browsers started doing this because of security reasons.
Combine this with the benefits of the HTTP/2 protocol, and from a performance perspective you don't have any reason anymore to fetch font files from other domains.
But I want to link to Google Fonts
Ok, ok, I hear you. If you really want to use Google Fonts, then prevent it from being render blocking, as you're not in control of the Google servers, nor user and device conditions.
The general advice then is to lazyload the Google Fonts CSS by using the CSS lazyloading strategy mentioned before. You should throw in resource hints to reduce the gap between first render and fonts being downloaded and rendered. A year ago, Harry Roberts wrote an article on the fastest Google Fonts. The async snippet with resource hints turned out to be the winner.
Font-display: swap
And yes, do use font-display swap. Because even when not using any font-display
strategy, you would and up having the same layout shift anyway. And without fonts being visible, user engagement is less likely to happen at early stage.
Variable fonts
Variable fonts are different font weights and variations of the same font, packed in one font file. And back in the days, if we wanted to optimize, reducing the amount of resources was the number one method.
But with HTTP/2, this isn't needed anymore. Variable fonts could actually become quite big. So you might be better of using 3 or 4 individual files, while only preloading those needed above the fold.
Next to browser support, it really depends on your use-case if variable fonts comes with performance gains.
Controlling the font loading experience
The FontFace API is a browser native API that one can use to control the loading experiences. That's correct, just like the WebFontLoader library. But you'll end up using less JavaScript, as illustrated in this FontFace coding example.
Using this, you can set a class to the body once the Font is done downloading. And then start applying the custom font as illustrated in the coding example.
Reduce noticeable font shifts
Additionally, as long as the class isn't added yet, you could adjust letter-spacing and line-height of the fallback font to mimic characteristics of the expected custom font. For the latter, the following CSS could be used:
body:not(.bitter) {
/* mimic characteristics */
}
This could reduce noticeable shifts (in some cases even precenting vertical layout shifts, impacting your CLS metric).
The pure CSS solution
There actually is a new CSS-only solution in town to reduce and maybe even prevent fonts and layout shifts. And I have yet to implement this in my own website. It's called f-mods, or font descriptors.
Additionally, I'm checking the internet speed and data usage preferences. Based on those, a piece of JavaScript will decide to not load nor apply the fonts.
However, if you started preloading them, they will be downloaded anyway. So you then have to come up with a solution to prevent this as well (which I did 😆)
In my opinion, icon fonts should always be lazyloaded using the print=media 'hack'.
I've seen merchants doing this, so going to share it right away:
I wouldn't advice to switch to full inlined SVG's as it could result in chunked HTML and all related bottlenecks.
Other font optimizing considerations
Although I believe the above is explaining the basics, there is way more to tell about fonts. And if you are craving for more in-depth articles on fonts, then here is some further reading:
- by Simon Hearne;
- by Barry Pollard;
- by Google (Barry Pollard and Katie Hempenius);
- by Matt Hobbs on the importance of face-source-order;
Any other articles that should be mentioned here? Let me know!