Ejecting from Google Fonts

When I began to design this blog, I chose and installed custom fonts (one for text, one for code) as a quick win to get some momentum.

I went to Google Fonts, chose Fira Code for code blocks (easy choice: it's my preferred editor font!), and then eventually chose Fira Sans for regular text.

Google Fonts makes the embed process really easy. After making my selections, the following code was generated which I then added to my main page template:

...along with a reference to each font in my styles:

As a starting point, this is reasonably-performant:

  • the <link rel="preconnect" ... /> warms up a connection to the CDN that the fonts are served from, and the font files are properly cached
  • the stylesheets that are returned specify font-display: swap, which instructs the browser not to block on the font resource, and to swap it out once it becomes available (docs here if you're curious)
  • the stylesheets that are returned favor a local version of the font, if it exists

Upon further inspection — while running a Lighthouse report — I noticed some feedback in the report that the fonts were considered render-blocking resources.

What does "render-blocking" mean, and how bad is it?

Reading the page for the issue mentioned in the Lighthouse report, I saw this listed in the definition of "render-blocking resources":

A <link rel="stylesheet"> tag that:
  • Does not have a disabled attribute. When this attribute is present, the browser does not download the stylesheet.
  • Does not have a media attribute that matches the user's device.

...and realized that while the generated code from Google Fonts works, it's missing an indication of which media we intend to target (the second case). This is easy enough to fix:

...but it also got me thinking: is this the best way to manage fonts?

  1. Do I want to rely on Google hosting the styles / fonts?
  2. Is the extra roundtrip for the styles before even loading the fonts tenable?
  3. What is the ideal user experience under various conditions?

External dependencies, and Murphy's Law

On a long enough timeline, the risk of any external system being down or unavailable is non-zero... eventually there will be an issue, and it will prevent the fonts from loading.

How big of a deal is that though, really? We're talking about a slightly-degraded user experience on a blog site that I'm not even sure has any real readers...the pages will still render just fine, albeit with a look that doesn't spark as much joy for me.

What's perhaps more interesting is the question underneath: if I'm already using Netlify as a CDN, why rely on something else?

Pausing on the question of where these font files should come from, I also stopped to consider the trade-offs in user experience.

What's the ideal user experience?

There are essentially two choices for handling the case where the font files are not already cached:

  1. block all rendering until the font is available (which leads to FOIT), or...
  2. render once with a default font, and then render again with the desired font once it's available (which results in FOUC)

I leaned in this case towards FOUC. I can't actually think of any scenario to use FOIT, but like most things I assume it exists as an option because it's occasionally useful.

With a solid browser-caching strategy, even FOUC should be a relatively infrequent experience... users would download the fonts the first time they need them and then the flicker would be a non-issue.

I was happy to file these ponderings under "things that might matter, but can certainly wait", but while looking into it I found an excellent read about tuning up Google Fonts, so down the rabbit hole I went.

A new approach

The first win from that article was the pointer towards google-webfonts-helper, which is a web app that helps you use Google Fonts in a self-hosted way: you choose your fonts, and which browsers you want to support, and it generates (better!) implementation code and a .zip of the assets.

In a few minutes I had removed the external stylesheets that reference Google's CDN, and had added the font files as local assets and updated the references to them. This is going great.

After pushing the changes and getting a new Netlify preview though, I was seeing the FOUC on subsequent visits. Some quick Googling led me to a thorough breakdown of Netlify's caching strategy, which offered some insight in to what I was seeing and how I might go about getting the behavior I want.

Trade-offs, always trade-offs

Netlify's promise of atomic deploys is a huge part of their value: I can't imagine doing what they do any other way.

The core premise is that if my assets change, I don't want them cached on users' machines (imagine a critical bug in a JS file that can't be easily updated by deploying a new version).

This fonts use case feels like a legitimate exception though... I'm not going to ever update the content in a font file, right? I suppose I might choose to use a new font someday, but I'll need to reference a different file in that scenario anyhow.

This is a case where I actually want to cache the file on the user's machine, which Netlify supports well. Using the Netlify config example as a guide, I added the necessary code to add HTTP caching headers for the font files to netlify.toml:

This adds a header to the HTTP response for a font file that effectively states "this resource should be cached on the client for a year".

A quick push and deploy later, and I was able to confirm the font files were being cached in the browser. Success!

A final improvement

This is definitely better. I don't have to worry about a third-party resource being unavailable, and the flicker is a non-issue after the fonts get loaded the first time. I was ready to consider this done until that part of my brain that can't leave well-enough alone asked "what about that preloading we had before"?

Remember that in the original version from Google Fonts, there was this

element included, which tells the browser to begin the hand-shake with the referenced domain.

Now that I'm self-hosting the fonts alongside everything else, that approach doesn't make sense. What we can do instead, however, is to change that pre-connect logic to a proper preloading instruction for the actual file assets:

The browsers that support preloading hints all support the .woff2 font format, which means we don't need to try to preload the other formats that came from google-webfonts-helper. I'm ok with this amount of overlap, if you want to support additional browsers you can certainly add more preload hints, but remember to be careful about only preloading assets you actually intend to be used.

That's it, an ideal way to get the convenience of Google Fonts without the run-time risks. Thanks for reading!