This post was originally published on my blog page at try.catch.wtf
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* 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.
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 has a great article on it.
You can use Variable Fonts 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 * using glyphhanger.
# 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 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.
# 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.
@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.
@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. Use swap
only when the font is absolutely necessary. Make sure to deliver the font early enough to prevent layout shifts.
block
Displays invisible text for a. Once the custom font downloads, it swaps the font. It can cause 'Flash of Invisible Text' or FOIT.
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.
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.
<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.
<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 acrossorigin
attribute because, unlike stylesheets, font files are served over CORS connection.
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! 👋😽
Top comments (1)
Very detailed article.