How We Load Web Fonts Progressively

Note: This article’s title was updated for clarity. It was formerly called “Font Loading Revisited with Font Events.”

Last month we wrote about an approach we’d been using to load web fonts in a more responsible manner than browsers tend to do by default. The purpose of the approach was to avoid a typically undesirable browser behavior we often refer to as the “FOIT” (Flash of Invisible Text), in which a browser hides all text that should be styled with a custom font until that font has finished loading.

A brief recap on the FOIT

Permalink to 'A brief recap on the FOIT'

The FOIT tends to be most problematic in browsers like iOS Safari, which hides text for up to 30 seconds before giving up and rendering it with a default font, but it can also be seen in browsers with shorter hiding durations like Chrome, Firefox, and Opera as well. For example, here’s how our site would load in Chrome on a 3G-ish connection if we were loading fonts in a standard way through CSS @font-face. Note that the page content is available for rendering at around 1.7 seconds in the timeline, yet the text is hidden until fonts have finished loading.

Timeline of our website using standard font loading. On a 3G connection.

FIG 1: Timeline of our website using standard custom font loading. On a 3G connection.

It’s nice that these browsers limit their text hiding to 3 seconds, but even 3 seconds is a long time to wait for content that’s already downloaded. After all, the performance community tends to agree that 1 second is a reasonable goal for rendering a usable page on a fast connection, and we tend to shoot for rendering something useful in 2 seconds on slower devices on mobile networks as well. Since a page typically requires text to be usable, FOIT timeouts are a concern across most browsers, not just iOS Safari. But again, that browser’s behavior is by far the worst.

A workable workaround

Permalink to 'A workable workaround'

Our initial approach to working around the FOIT involved converting font files into Data URIs so that those fonts could be embedded directly into font-face definitions in a CSS file. By loading that CSS file in an asynchronous manner (using a bit of JavaScript), we could ensure that a browser would immediately render the text in the page using fallback fonts, and the custom fonts would apply once the CSS finished loading.

An unexpected side effect

Permalink to 'An unexpected side effect'

That approach seemed to serve us well, but we recently started noticing that sites using this particular approach sometimes exhibited a small side effect: a brief FOIT or blink that happens long after the fonts finish loading, just before they are applied to the page. It didn’t seem to happen all the time, but we’d been seeing it crop up more and more regularly. The page loading timeline below of our site accessed over a fast connection while using this approach displays this peculiar blip.

Timeline of our website using data uri font loading

FIG 2: Timeline of our website using async-loaded data URI fonts.

In this timeline, the page is usable (with fallback fonts) at around 600ms, but then for about 100ms, a FOIT occurs before the page ultimately returns to a usable state at 800ms. Looking at the request timing, we could see that the FOIT began just after the fonts finished loading. Of course, 800ms is a very fast page load, and a 100ms blink may not be the end of the world, but we found that the problem tended to display more prominently on slower connections and devices and on other sites as well. For example, here’s the same load on 3G:

Timeline of our website using data uri font loading. 3G connection.

FIG 3: Timeline of our website using async-loaded data URI fonts. 3G connection.

Okay, okay. So another 100ms of hidden text still isn’t terrible, but it was certainly odd. And we sometimes saw longer delays of 500ms or so on client sites that were utilizing more custom fonts than our site does.

Something was up, and it seemed we had a little more work to do.

Finding the source of the problem

Permalink to 'Finding the source of the problem'

At first we wondered if the blink was just an artifact of repainting/reflowing a moderately complex layout. But the characteristic hidden text alongside visible graphic elements sure looked like your run-of-the-mill FOIT, and sure enough, it was. In short, a FOIT happens when a browser attempts to style an HTML element with a font-family that is defined (via a @font-face declaration) but not yet loaded. Interestingly enough, in this case it appeared that although the CSS and its included fonts had indeed already been delivered over the network to the browser, the browser still seemed to hide the text while parsing the data URI string, which we know can take a little time, particularly on slower devices.

Enabling fonts when they’re really loaded

Permalink to 'Enabling fonts when they’re really loaded'

After realizing that even data URIs can introduce a FOIT while they’re being parsed, we wanted to ensure that we applied our fonts to the page only after they were truly ready to render. Fortunately, Zach here had recently written a great article over at Dev.Opera about an upcoming font events API that is designed for this specific purpose, and there are some nice lightweight polyfills available (1, 2) to start experimenting with it today. To experiment with a font events approach, we gave Bram Stein’s FontFaceObserver script a try.

Here’s how it looks to use fontfaceobserver to set up a loading listener for one of the fonts used on this site (Open Sans Pro):

new w.FontFaceObserver( "lato" )
	.check()
	.then( function(){ console.log( “Loaded!” ); });

You can read more about how to use Bram’s script on Github, but in short, you can specify a font family and other identifying details such as the font’s weight and style. Once the observer is created, you just need to check() it, and then get a callback when it finishes loading (which is easy to do through the then() method). Neat!

With this tool in hand, we followed a clever idea from Zach’s Opera article to qualify the use of our fonts throughout the site through a class selector, which we could add once the fonts were loaded. For example, the body element would reference a fallback sans-serif font until a class of fonts-loaded was present on the html element:

body {
	font-family: sans-serif;
}
.fonts-loaded body {
	font-family: lato, sans-serif;
}

And our font observer callback can add that class when the font loads!

new w.FontFaceObserver( "lato" )
	.check()
	.then( function(){
		w.document.documentElement.className += " fonts-loaded";
	});

With that logic in place, our data URI fonts layered in without a blink. Great!

Timeline of our website using async-loaded data URI fonts with font events. No FOIT!

FIG 4: Timeline of our website using async-loaded data URI fonts with font events. No FOIT!

Great. So why the URIs?

Permalink to 'Great. So why the URIs?'

At this point, we had a fix, but it got us thinking that maybe we didn’t need to go through the whole data URI route anymore, now that we were using a font events polyfill anyway. After all, these new font loading and listening tools are designed to help load fonts referenced via regular old CSS @font-face declarations, and who knows, maybe that’d be simpler and faster as well.

To make the switch, we removed the CSS file containing our data URIs, and the JavaScript logic we had used to load that CSS, and placed standard @font face rules referencing Woff2, Woff, and TTF files in our CSS, like this:

@font-face {
    font-family: 'lato';
    src: url('/css/type/lato-light-webfont.woff2') format('woff2'),
         url('/css/type/lato-light-webfont.woff') format('woff'),
         url('/css/type/lato-light-webfont.ttf') format('truetype');
    font-weight: 300;
    font-style: normal;
}
 body {
	font-family: sans-serif;
}
.fonts-loaded body {
	font-family: lato, sans-serif;
}

Conveniently, the FontFaceObserver script will actually load a font for you when you call it, as long as the font you’re observing is referenced in a @font-face declaration in the CSS. It does this by generating an HTML element and styling it with the font you’re referencing, which causes the browser to kick off a request to its format of choice.

The Result: Faster than ever!

Permalink to 'The Result: Faster than ever!'

By referencing our fonts using CSS @font-face and using font loading APIs to load and enable them when ready, we’ve found our fastest page load yet (complete in 600 milliseconds on wifi!) while retaining the progressive font rendering we desired.

Here’s a timeline of our homepage now, loaded over a wifi connection.

Timeline of our website using  and font events. On a wifi connection.

FIG 5: Timeline of our website using `@font-face` and font events. On a wifi connection.

Last step: Optimizing return visits

Permalink to 'Last step: Optimizing return visits'

For return visits, we wanted to see if we could enable the font as soon as possible, knowing that it’d likely be cached in the browser from a prior load. To do this, we simply set a cookie after the fonts finish loading on the first visit. Next page view, the server side checks for that cookie and sets the fonts-loaded class in the HTML source that’s delivered to the browser. Using SSI for example, that looks like this:


<!--#if expr="$HTTP_COOKIE=/fonts\-loaded\=true/" -->
<html lang="en" class="fonts-loaded">
<!--#else -->
<html lang="en">
<!--#endif -->

And with that tweak, our return visits look as fast as can be. We have a complete render at 300ms on a fast connection:

Timeline of our website's return visit timing on wifi.

FIG 6: Timeline of our website's return visit timing on wifi.

But what’s particularly nice about this is that it is able to optimize all further browsing on the site, not just revisiting a page a user has already seen.

Looking ahead

Permalink to 'Looking ahead'

By using font events and a clever polyfill, we were able to get our fonts to load progressively, saving our users from the dreaded FOIT. Looking ahead, we may be able to specify that our fonts load this way with a simple font-rendering CSS property instead of having to bother with JavaScript font APIs at all. For example, to specify that fonts should load progressively, as we’ve designed above, we can simply specify the following in our CSS: font-rendering: swap;. Of course, like any new property, it’ll be a while before we can rely on that behavior to work across a broad number of devices. Our biggest worry these days is iOS Safari with its incredibly annoying FOIT delay, so here’s hoping Apple gets on board with this new approach in an upcoming version!

For more on how that evolves, we’ll keep our eyes on this draft specification: CSS Font Rendering.

Thanks for reading along!

Permalink to 'Thanks for reading along!'

If you’re interested, we posted an example page on github to show how things are set up.

All blog posts