Web Font Loading & The Status Quo
Let's start with the obvious: there's lots of great posts out there on font loading (which all tend to be 27 pages long for some reason) and using the font-display
CSS property, and… you get the idea.
These all accept the status quo—that fonts cannot load synchronously like your CSS—and just describe ways to mask that.
But, it's my website, and I know exactly what fonts the user is going to need. So why can't I ask the browser to put a small font onto the critical path before a page displays at all? As an engineer, I find the lack of choice frustrating. 😠
I don't have a perfect soution, but this post lays out my gripes, a fallback solution via base64 encoding your fonts, and platform suggestion. To start, here's the fundamental issue, shown via animation.
While there's variants on this problem, there's two things happening here:
- "Emojityper" displays with the system font first
- The loaded font is bigger than the system font—we see layout shift, which I'm paid by my employer to tell you is bad (it is bad, but I'm also paid to tell you)
The status quo solution is to use the font-display
CSS property (and some friends).
And to be fair, traditional CSS can solve both of these problems.
However, these issues are typically solved by not displaying the offending text until its font arrives—even though the rest of your page is rendered.
The most frustrating issue here is that this "flash" takes all of a few frames—maybe 50-60ms. This is the choice I'd like: to delay rendering by a small amount of time. My opinion on this UX is that users are going to be more delighted by a page ready-to-go rather than one effected by a flash that confuses a user's eyes for mere milliseconds. 👀
Case Study
On developer.chrome.com, we actually inline all of our stylesheets and images (largely SVGs) into each page's HTML in order to reduce the number of requests and make the page load faster. We're really happy with this solution, because for most users, their network is going to deliver that whole single payload incredibly quickly.
Despite this sheer duplication of assets across every HTML page, our fonts still go to the network, and new users will still see a flash.
Loading in general
For background on loading, see my recent interactive post. The TL;DR from that post is that the only thing that can block a page from rendering is loading external CSS. And for fonts, your browser will asynchronously load a font when glyphs from it are needed—e.g., for the heading font of this blog, that's immediately, but only once the stylesheet has first arrived.
Here, I'm actually using two tricks to get you the font earlier (although neither prevents the flash and layout shift):
- I use
<link rel="preload" ... />
to request the font early, although this only helps if you have an external CSS file (if it's inlined in<style>
, the font URL is right there) - I also send the font via HTTP2 Server Push before any HTML goes to the user, although it seems like browser vendors are removing support for this due to misuse
Regardless of what you think this post, preloading your font is a good idea. Modern HTTP is very good at sending you lots of files at once, so the earlier your user's font can get on that train, the better. 🚂🚋🚋
Font files should also be fingerprinted and cached forever for future loads. I digress, but this loading issue—like so many—is only about the user's 1st load. With the advent of service workers, we as web developers have almost complete control over the user's 2nd load.
Solutions, today
This is a tricky one. We can actually include a font inline in your blocking CSS file—by base64 encoding it, which has ~33% space overhead. There's no extra network requests here and decoding is done in a blocking way.
@font-face {
font-family: 'Carter One';
src: url('data:application/font-woff2;charset=utf-8;base64,d09GMgABAAAAAG74ABI...') format('woff2');
font-weight: normal;
font-style: normal;
}
Many folks argue that base64 is a bad idea. Although, in that case study, the size of the image isn't listed—about 220k—and the author fundamentally disagrees with my assertion that fonts can be critical resources.
There is cost here, both in space and decoding time. If you're going to base64 a font to avoid the flash, how can you minimize the cost?
-
I find that most Latin custom fonts are about ~20k, and I wouldn't base64 anything substantially larger than that—keep it to a single font at most. (I'd use the system font for body text, and leave a custom font for your headings or hero text.)
-
Put the font declaration in a unique CSS file that's cached forever. Unlike the rest of your CSS, which you might change, the font is not going to change over time.
<!-- These will be downloaded in parallel -->
<link rel="stylesheet" href="./base64-encoded-font-eeb16h.css" />
<link rel="stylesheet" href="./styles-cakl1f.css" />
-
Only ship woff2—95%+ of users have support
-
This is advanced, but if you can control what your user gets on their 2nd load (e.g., via a Service Worker), then you could serve the user a real, cached woff2 as well and then use only it for repeat loads.
Anti-patterns
There are other ways to ensure users don't see any part of your page before the fonts load. But they're going to involve JavaScript and that's just a rabbit hole that increases your site's complexity real fast. 📈
You could mark every part of your page as hidden via a CSS class, and then only remove it once you see a font arrive.
You could do this via the Font Loading API or by literally measuring the rendering size of a test <div>
until it changes.
These are not good solutions.
(This is something I happily do on Santa Tracker, but we literally have a loading screen, lean in to a slow load, and the entire site requires JS. It's not suitable for sites.)
A standards plea
What would solve this problem and make me happy? Last year, a proposal was made to add Priority Hints. Right now, this proposal is just for hints about the importance of network traffic.
But perhaps it could include a hint choice of critical
which informs a browser that this preload may block page rendering—if it arrives quickly, of course.
<!-- Preload this font and block until used, with limited budget -->
<link rel="preload"
importance="critical"
href="/carter-one.woff2?v11"
as="font"
type="font/woff2"
crossorigin />
<!-- This could work for as="style", as="fetch" or others -->
<link rel="preload"
importance="critical"
href="/important-data.json"
as="fetch"
crossorigin />
This would allow for standards-based developer choice, and because it's a purely additive attribute, would have a sensible fallback for unsupported browsers (i.e., not to block the page at all). There's also a wide range of resources you can preload, so it could be a versatile tool. ⚒️
Summary
I find a lack of control over font loading frustrating, and using base64 for small fonts can help you if this problem frustrates you too. And if you find yourself trying to preload similarly-sized images 🖼️ to make your page work, that's actually one of the biggest signs this approach might help you—to me, that font is just as important as that site logo or navigation button. 🍔
To be clear, this can be a footgun—don't block page loading for minutes because 100k of fonts haven't arrived—use base64 sparingly to avoid a flash or layout shift. I don't think it makes sense for every site. I'm not even sure I'm going to implement this strategy on this blog.
Yet, to revisit the developer.chrome.com case study from earlier, where we happily inline images and our stylesheets. I don't think we should inline the fonts directly on the page—they're ~20k files which never change—but moving them to a synchronous, fingerprinted (and cached forever), stylesheet including just the base64 font may be on the cards.
➡️ Let me know what you think on Twitter.
Thanks to Šime for some feedback on this article.