How we use web fonts responsibly, or, avoiding a @font-face-palm
02/16/2015 Note: There’s an update to this article that recommends a slightly better approach. You can find it here: Font Loading Revisited with Font Events
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.
The problem with @font-face
Permalink to 'The problem with @font-face'
The CSS @font-face
declaration is the standard approach for referencing custom fonts on the web:
/* Define a custom web font */
@font-face {
font-family: 'MyWebFont';
url('webfont.woff2') format('woff2'),
url('webfont.woff') format('woff'),
url('webfont.ttf') format('truetype'),
}
/* Use that font in a page */
body {
font-family: 'MyWebFont', sans-serif;
}
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:
- The CSS request containing your
font-face
definition(s) should not block page render. Instead of referencing your fonts via<link>
s in the<head>
or via@import
statements in an external stylesheet, try to load your fonts and font content asynchronously. Don’t worry, we’ll show you how. - Font requests should be set up to ensure the fallback text is visible while loading, avoiding the Flash of Invisible Text or FOIT.
The Filament Group Way™ to load fonts
Permalink to 'The Filament Group Way™ to load fonts'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.
Step 1: Prepare your fonts
Permalink to 'Step 1: Prepare your fonts'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.
Step 2: Prepare the font stylesheets
Permalink to 'Step 2: Prepare the font stylesheets'Encoding fonts to Data URIs
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):
- OpenSans-Regular.ttf
- OpenSans-Bold.ttf
- OpenSans-Regular.woff
- OpenSans-Bold.woff
- OpenSans-Regular.woff2
- OpenSans-Bold.woff2
If you’re missing one or more of these formats, upload it into the Font Squirrel Web Font Generator to create the others for you.
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:
@font-face {
font-family: Open Sans;
src: url("data:application/x-font-woff;charset=utf-8;base64,...") format("woff");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: Open Sans;
src: url("data:application/x-font-woff;charset=utf-8;base64,...") format("woff");
font-weight: 700; /* Bold */
font-style: normal;
}
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");
.
Step 3: Set up the Stylesheet loader
Permalink to 'Step 3: Set up the Stylesheet loader'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.
Here’s an excerpt of the JavaScript we use to load our fonts. We recommend placing this JavaScript inline in a script
element in the head
of your HTML to kick off the font request as soon as possible (more about how we configure the head of our pages with Enhance.js):
// NOTE!! The WOFF2 feature test and loadCSS utility are omitted for brevity
var ua = window.navigator.userAgent;
// Use WOFF2 if supported
if( supportsWoff2 ) {
loadCSS( "/url/to/data-woff2.css" );
} else if( ua.indexOf( "Android 4." ) > -1 && ua.indexOf( "like Gecko" ) > -1 && ua.indexOf( "Chrome" ) === -1 ) {
// Android's Default Browser needs TTF instead of WOFF
loadCSS( "/url/to/data-ttf.css" );
} else {
// Default to WOFF
loadCSS( "/url/to/data-woff.css" );
}
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):
Using Cookies to make this Smarter
Permalink to 'Using Cookies to make this Smarter'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:
// NOTE!! The WOFF2 feature test, loadCSS, and cookie utility are omitted for brevity
// Default to WOFF
var fontFileUrl = "/url/to/data-woff.css",
ua = window.navigator.userAgent;
// Use WOFF2 if supported
if( supportsWoff2 ) {
fontFileUrl = "/url/to/data-woff2.css";
} else if( ua.indexOf( "Android 4." ) > -1 && ua.indexOf( "like Gecko" ) > -1 && ua.indexOf( "Chrome" ) === -1 ) {
// Android's Default Browser needs TTF instead of WOFF
fontFileUrl = "/url/to/data-ttf.css";
}
// ADDED: Make sure the fonts are not yet cached
if( fontFileUrl && !cookie( "fonts" ) ) {
// Load the fonts asynchronously
loadCSS( fontFileUrl );
// ADDED: Set the cookie indicating the fonts are cached
// The cookie also denotes what format is used (WOFF, WOFF2, or TTF)
cookie( "fonts", fontFileUrl, 7 );
}
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.
<!--#set var="fontsWOFF" value="/css/data-woff.css" -->
<!--#set var="fontsWOFF2" value="/css/data-woff2.css" -->
<!--#set var="fontsTTF" value="/css/data-ttf.css" -->
<!--#if expr="$HTTP_COOKIE=/fonts\=$fontsWOFF/" -->
<link rel="stylesheet" href="<!--#echo var="fontsWOFF" -->">
<!--#elif expr="$HTTP_COOKIE=/fonts\=$fontsWOFF2/" -->
<link rel="stylesheet" href="<!--#echo var="fontsWOFF2" -->">
<!--#elif expr="$HTTP_COOKIE=/fonts\=$fontsTTF/" -->
<link rel="stylesheet" href="<!--#echo var="fontsTTF" -->">
<!--#endif -->
The code above requires Apache Server Side Includes but you could do something similar with any server side language.
Wrapping Up
Permalink to 'Wrapping Up'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!