How we use web fonts responsibly, or, avoiding a @font-face-palm

Using @font-face to load custom web fonts is a great feature to give our sites a unique and memorable aesthetic. However, when you use custom fonts on the web using standard techniques, they can slow down page load speed and hamper performance—both real and perceived. Luckily, we’ve figured out some methods to apply them carefully to ensure your site correctly balances usability, performance and style.

Clean and simple, but unfortunately most browsers’ default handling of @font-face is problematic. When you reference an external web font using @font-face, most browsers will make any text that uses that font completely invisible while the external font is loading [Fig. 1, below]. Some browsers will wait a predetermined amount of time (usually three seconds) for the font to load before they give up and show the text using the fallback font-family. But just like a loyal puppy, WebKit browsers (Safari, default Android Browser, Blackberry) will wait forever (okay, often 30 seconds or more) for the font to return. This means your custom fonts represent a potential single point of failure for a usable site.

Even when the fonts do load correctly, custom fonts slow down the perceived speed of a site significantly because a page full of invisible text isn’t exactly usable. Sure, once the first page is visited, the custom fonts are cached and display quickly, but perceived speed for the first page view is critical. If we can’t paint a usable page within a few seconds, a lot of visitors will drop off.

For example, Fig. 2 shows a webpagetest.org timeline illustrating how filamentgroup.com would look when accessed on a stable 3G connection if it were using the default font loading behavior, note that the custom @font-face text does not appear until a full second after first render:

Our users want a usable page as quickly as possible—within a second, ideally—so we want visible text as close to that goal as we can. There are several approaches you can take to work around these issues, but the most important thing you can do is to move away from the default way we’re told to load fonts.

Here are the criteria you should use when evaluating a font loading approach:

To optimize for the first view, we first make sure we have a native font in our font-family stack behind our custom web font, in this case font-family: Open Sans, sans-serif;. This sets the stage for how our text will render using the fallback experience while the font is loading using our new font loading method. JavaScript can then used to detect the best font format to use (WOFF2, WOFF, TTF) and asynchronously load a stylesheet that contains all the fonts embedded as a series of data URIs. This is a bit unconventional but it allows us to load all the custom fonts as a single HTTP request, which is nice both for minimizing reflows (all fonts arrive at once) and for reducing HTTP requests in general. To take this even further, after requesting a font, we set a cookie to flag that the custom fonts are now cached so we can avoid the flash of the default fonts on subsequent pages.

Custom fonts can be very heavy so the first order of business is minimizing the number of fonts we need to load in the first place. Remember each weight (regular, light, bold) and variant (regular italic, bold italic) of a typeface is a separate font file which can add up quickly. Try to keep the total number of custom fonts to less then five, but we usually shoot for 2-3 if we can.

To further streamline your font delivery, use a technique called subsetting that allows you to remove characters and symbols from a font that you don’t need. The FontSquirrel tool makes this pretty easy.

Let’s say we’re using the Open Sans typeface with two different weights: 400 and 700 (Bold). To support the widest range of browsers, we’ll need each font in three different formats: WOFF2, WOFF, and TrueType (TTF):

Take each of these font files and encode them into a Data URI so we can embed them into a stylesheet. If you aren’t familiar with how to create a Data URI, there are many options: SASS (Compass), PHP, online generators, or by using OpenSSL on the command line (openssl base64 -in filename.woff).

Copy the output into three different stylesheets, one CSS file for each font format: WOFF2 (data-woff2.css), WOFF (data-woff.css), and TTF (data-ttf.css for Android). Here is what an example of data-woff.css might look like:

Inside of the other font format files, the src: url(...) format(...) should match up with the specific format. For example, inside data-woff2.css you’d use url("data:application/font-woff2;charset=utf-8;base64,...") format("woff2"); and for data-ttf.css you’d use url("data:application/x-font-ttf;charset=utf-8;base64,...") format("truetype");.

Once a font file is prepared, we’ll need to load it asynchronously to ensure no FOIT occurs. We use our our loadCSS utility to handle this part. For example, here’s how we can use loadCSS to load our WOFF2 fonts:

loadCSS( '/url/to/data-woff2.css' );

Of course, we want to load the appropriate font stylesheet for each browser that visits the site. How do we determine which format to use? By default we use the WOFF format because of its breadth of browser support. If a browser passes a WOFF2 feature test, we use WOFF2 instead because its file size is normally about 30% smaller. If we can reasonably guess that the current browser is the defualt Android Webkit Browser (not Chrome), we switch to TTF for Android 4.X support. Keep in mind that if an incorrect format is loaded, the browser will simply fallback to using default local fonts.

The browser will not make the text invisible while our Data URI CSS file is loading asynchronously. This means that the fallback text will be readable while our web fonts are loading—even if the request hangs and never returns.

Figure 3 below shows the change: With this technique we get readable immediately on first render. This is what we’re going for (3G timeline):

Up to this point, we’ve focused on preparing and loading our custom fonts responsibly so we can show the fallbfgack font while we wait for the custom fonts to load. When these fonts finally do load, the browser swaps out the native fonts for custom fonts. This will cause a repaint and usually has small layout shifts since the fonts are slightly different sizes. We think this font shift is a small tradeoff to show a usable page seconds faster on the initial visit but it can be annoying once you start navigating around.

To remedy this, we use cookies to track if the custom fonts are already downloaded and in the browser’s cache. If they are, we show the custom fonts right off the bat to avoid any shifting around.

Instead of the font loader above we’ll want to use a different loader, shown below. We’ll want to add a cookie to flag that the fonts are now cached. In addition to noting that the fonts are cached, the cookie also contains the URL to the specific font format being used as well (data-woff2.css, data-woff.css, or data-ttf.css). Don’t forget to include the Filament Group cookie utility:

Then add the following markup block to our <head> updating the values of each of the fontsWOFF, fontsWOFF2, fontsTTF variables with the URL of the Data URI CSS font format file. Note that when the cookie has been set and contains the value of the URL of the font format we want to load, a blocking link element is inserted into the page pointing to the Data URI CSS file. However, the blocking behavior of this request is okay because the CSS request has already been cached by the browser and it will load almost immediately.

Using web fonts can really be a great way to improve the quality of our web work, but using web fonts with the default loading behavior can be very detrimental to our page’s perceived performance. The above method works great to eliminate the text invisibility usually associated with @font-face and make our pages usable much faster. We hope you (and your visitors) find it useful!