Bram Stein (@bram_stein) is a web developer working on web font serving at Adobe Typekit. He cares a lot about typography and design on the web, and is happiest working at the intersection between design and technology. In his spare time he works on the State of Web Type, Font Face Observer, and several other tools for improving web typography. He writes about web typography for several publications and speaks at conferences around the world.
Typography is an integral part of design, so it’s no wonder web fonts usage soared once browsers started supporting them. Based on the statistics gathered by the HTTP Archive, nearly 65% of all websites use web fonts as of November 2016. This is great.
But there’s a downside: web fonts have become a performance and accessibility bottleneck due to the way most browsers implement font loading. By hiding text while web fonts are loading, they are denying your visitors access to your content until the web fonts have finished loading. This is especially aggravating on slower network connections, where text can be invisible for several seconds. This problem is called the Flash Of Invisible Text (FOIT). Because of this, there has been a recent backlash against web fonts in favour of locally installed fonts.
It’s true. Hidden text is a problem, and it’s a perfectly acceptable solution to not use web fonts. Not every website needs web fonts. They will never load as quickly as locally installed fonts, and, sometimes, they just don’t fit in your performance budget. That’s fine.
However, hidden text is not inherent to web fonts. It’s how browsers like Firefox, Chrome, Opera, and Safari chose to implement web font loading. Other browsers, like Microsoft’s Internet Explorer and Edge went a different route. They immediately show the text in a fallback font until the web font loads. This is called the Flash Of Unstyled Text (FOUT). FOUT provides a much better experience for people on slow network connections. They get to see the text as soon as possible, and the web font when it loads. FOUT treats web fonts like progressive enhancement. It shows the fallback fonts first, and then enhances that with web fonts. This is a good thing.
As developers, we can’t easily influence how the browser loads fonts. Fortunately, the W3C is working on a new descriptor called font-display
that makes font loading configurable through CSS. The descriptor takes four values: block
, swap
, fallback
, and optional
(and the default auto
). The block
value hides text while web fonts are downloading. The swap
value shows the fallback font first, followed by the web font when it loads. The fallback
value is like swap
, except that it has a timeout. If the font doesn’t load within the timeout, the fallback font continues to show. The optional
value only shows the web font when it is available within 100ms (for example, when it is loaded from the browser cache, or from a very fast network connection). The font-display
property is added to a @font-face
rule, and influences the font loading for that font only.
@font-face { font-family: Source Serif; src: url(/path/to/sourceserif-regular.woff2) format("woff2"), url(/path/to/sourceserif-regular.woff) format("woff"); font-display: swap; }
The font-display
descriptor is currently behind a flag in Chrome and Opera. Once it has widespread support, loading fonts asynchronously becomes as simple as adding font-display: swap
to your @font-face
rules. Fortunately, we don’t need to wait for browsers to support font-display
. With a bit of JavaScript, you can implement asynchronous font loading in any browser. To start, include @font-face
rules for the web fonts you want to load in your CSS. In this example, we’ll use two styles from the same family.
@font-face { font-family: Source Serif; src: url(/path/to/sourceserif-regular.woff2) format("woff2"), url(/path/to/sourceserif-regular.woff) format("woff"); } @font-face { font-family: Source Serif; src: url(/path/to/sourceserif-bold.woff2) format("woff2"), url(/path/to/sourceserif-bold.woff) format("woff"); font-weight: bold; }
You may notice that I’ve only supplied the WOFF and WOFF2 font formats in the src
descriptor. That’s all you need for modern browsers. You can read more about why the so-called “bulletproof @font-face” syntax is deprecated by Zach Leatherman.
It’s important to understand that browsers only hide text when a web font is loading. So, even if you load the stylesheet with the @font-face
rules asynchronously, the browser will still hide the text.
We need to trick the browser into showing the fallback font first. We can do this by making the fallback font the default. Then, once the web font has loaded, we add it to the font stack. Because the font is already loaded at that point, it’ll display immediately. Let’s add two selectors to our stylesheet: the first selector sets the default fallback font, and the second selector adds the web font to the font stack when the fonts-loaded
class is present.
html { /* make fallback font default */ font-family: Georgia, serif; } .fonts-loaded { /* add web font to stack once it loads */ font-family: Source Serif, Georgia, serif; }
If you try this out in your browser, you’ll see the fallback font. Good, that’s your baseline experience. Now we’ll load the fonts asynchronously using a library called Font Face Observer. Font Face Observer is a small font loading library with excellent browser support (disclaimer: I wrote it).
We’ll load the Font Face Observer library asynchronously in the <head>
of the page. Once it has loaded we create two new instances of FontFaceObserver
: one for the regular style, and one for the bold style. We then call the load
method for each instance. The load
method returns a promise that is resolved when the font loads, or rejected when it fails to load. We then use Promise.all
to wait until both styles have loaded. Once both are loaded we add the fonts-loaded
class to the HTML element.
var html = document.documentElement; var script = document.createElement("script"); script.src = "/path/to/fontfaceobserver.js"; script.async = true; script.onload = function () { var regular = new FontFaceObserver("Source Serif"); var bold = new FontFaceObserver("Source Serif", { weight: "bold" }); Promise.all([ regular.load(), bold.load() ]).then(function () { html.classList.add("fonts-loaded"); }); }; document.head.appendChild(script);
If you load this in your browser, and emulate a slow network device (for example, a 3G connection) you’ll see the fallback fonts first, followed by the web fonts. Hurray, we’ve loaded fonts asynchronously on all browsers!
Problem solved? Not quite. You probably noticed the brief flash of fallback text each time you reload the page. The text, or worse, the entire layout moves around. The sudden change that happens when a fallback font is replaced with a web font is very distracting. This happens because the characters of the fallback and web font have different widths. The differences for individual characters may be small, but they add up to quite a large discrepancy over an entire paragraph. This is the primary reason most designers and developers object to asynchronous font loading.
You can alleviate this sudden change in layout by trying to match the size of the web font to the fallback font. A good tool for this is the Font style matcher by Monica Dinosaurescu. Once you find a font size that roughly matches your fallback you can use it to adjust the size of the web font after it loads. This is where the fonts-loaded
class comes in handy again.
.fonts-loaded { font-family: Source Serif, Georgia, serif; font-size: 1.21em; }
It’s rare for two fonts to have exactly the same widths for all characters, so it’s hard to completely get rid of the jump in layout. But, we can avoid it in most cases. When a font is loaded over a slow network connection it is unacceptable to hide the text. However, loading a font from cache takes only a very short amount of time, and hiding text for that brief amount of time is preferable. If we can figure out when a font is in the browser cache, we can use that information to apply different font loading behaviour.
A good approximation of the cache is session storage. Session storage is identical to local storage, except it gets wiped when the browsing session ends (for example, when you close the browser window). Because the fonts are likely still in cache during the same browser session, it is a good (albeit imperfect) approximation of the state of the cache. We can exploit this by setting a flag in session storage after the fonts load for the first time. Then on subsequent request we check if the flag is set. If it is, we immediately add the fonts-loaded
class, triggering the default font loading behaviour. If it’s not, we load the fonts asynchronously (and set the flag).
var html = document.documentElement; if (sessionStorage.fontsLoaded) { html.classList.add("fonts-loaded"); } else { var script = document.createElement("script"); script.src = "/path/to/fontfaceobserver.js"; script.async = true; script.onload = function () { var regular = new FontFaceObserver("Source Serif"); var bold = new FontFaceObserver("Source Serif", { weight: "bold" }); Promise.all([ regular.load(), bold.load() ]).then(function () { html.classList.add("fonts-loaded"); sessionStorage.fontsLoaded = true; }); }; document.head.appendChild(script); }
And there you have it. If you load the page for the very first time you will see the fallback font immediately, followed by the web font once it loads. Subsequent visits will load the web fonts from cache and show them immediately. It’s the best of both worlds.
You can further optimise your font loading by preloading the most important styles of your font. The preload specification standardises a way for web developers to tell the browser that a resource should be preloaded. Preload instructions can be included as a <link>
element, or as an HTTP header.
<link rel="preload" href="/path/to/sourceserif-regular.woff2" as="font" type="font/woff2" crossorigin>
This is perfect for fonts because they are so important to rendering text as soon as possible. Instead of waiting until after the DOM and CSSOM are available, preload will force the browser to load fonts earlier.
So, to sum up: the default font loading behaviour in most browsers is terrible. You can’t just simply include @font-face
rules in your CSS (at least, not until the font-display
descriptor becomes widely supported). Instead, use a bit of JavaScript to load web fonts asynchronously. You can minimise the downsides of asynchronous loading by optimising for repeat visits and choosing appropriate fallback fonts. Help the browser discover fonts earlier by providing preload instructions.
By applying these principles you’ll take web fonts off the critical path and turn them into a true progressive enhancement. Your site’s visitors will thank you.
Thanks to Roel Nieskens and Zach Leatherman for reviewing this article and providing valuable feedback.