The @font-face dilemma
There has been a lot more discussion recently about web font performance, and deservedly so. Custom web font usage continues to grow, but the implementation often occurs without considering real-world performance. If you’ve spent any time on the web recently, especially without the comforts of high-speed internet, this may look familiar:
Basic @font-face
usage can frustrate your users and keep you from hitting more aggressive performance goals. I’ll go over @font-face
and how it can be problematic, different ways to improve font loading performance, and the prospects for better font loading in the near future.
How we got here
Starting in 1998 with Internet Explorer 4, and then from from March 2008 through March of 2010, one by one, all of the “big five” desktop browsers—Safari, Firefox, Opera, and Chrome—have rolled out roughly comparable implementations of @font-face font linking. - http://alistapart.com/article/fonts-at-the-crossing
Usage of @font-face
exploded in 2010. Font loading services took off. But the web was a lot different 5 years ago:
- A standard
@font-face
declaration needed EOT, TTF, WOFF, and SVG variations to work well across major browsers. - Mobile support was an afterthought. For example, when the iPhone 4 began shipping during the summer of 2010, Mobile Safari would crash if more than one weight or style of a font was loaded.
Today, the caniuse.com numbers for @font-face
are 92.92% globally and 97.89% in the United States. You generally only need WOFF to achieve cross-browser support. Unfortunately, there’s still a major web font problem from the early years that hasn’t been properly addressed: performance.
@font-face Browser Defaults
In cases where textual content is loaded before downloadable fonts are available, user agents may render text as it would be rendered if downloadable font resources are not available or they may render text transparently with fallback fonts to avoid a flash of text using a fallback font. - http://www.w3.org/TR/2011/WD-css3-fonts-20111004/#font-face-loading
While this section of the specification was not actually present until 2011, it’s useful in framing the current font loading landscape. Some time in 2009, Firefox and Opera began shipping @font-face
support with the former behavior: text would render with fallback fonts until downloadable font resources became available. But this choice frustrated many users (see the Firefox bug report) and was quickly dubbed FOUT, the Flash of Unstyled Text . Articles were written about fighting the @font-face
FOUT. It wasn’t long before most browsers were hiding text while fonts downloaded.
Unfortunately, the main issue with @font-face
now is what many wanted to avoid years ago: the FOIT, or Flash of Invisible Text. Consider this standard example:
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url(opensans.woff) format('woff');
}
body {
font-family: 'Open Sans', 'Arial', sans-serif;
}
Fallback fonts have been specified, but many browsers dictate that text should remain transparent until Open Sans has been downloaded or fetched from browser cache. Many Webkit browsers will wait 3 seconds before timing out and showing the fallback. Some browser may wait as long as 30 seconds, turning the flash of invisible text into an eternity for users on extremely slow network connections.
Improving font loading performance
Mobile users will absolutely experience some form of timeout on Edge, 3G, and what Jake Archibald has coined “Lie-Fi” networks: services like Airport and Hotel Wifi that often feel closer to a dial-up connection. To fix it, we need to load @font-face
in a way that doesn’t block render.
While some may fear a FOUT, the most performant option is to defer setting custom fonts via font-family until they’re available for use. It also just makes sense for most use cases, where readable content trumps custom fonts.
Font Loaders
You’ll need JavaScript, but a few small changes can make a big performance difference. Here’s a brief overview of some of the best options available now:
Typekit/Google Web Font Loader, 11KB minified
You may have worked with this loader at some point using hosted fonts from Google or Typekit. The Web Font Loader compares the widths of two spans while the page loads (check out the code) to determine if a web font is ready for use. Using the Web Font Loader asynchronously, like in this example below, avoids blocking the page.
<script>
WebFontConfig = {
typekit: { id: 'xxxxxx' }
};
(function(d) {
var wf = d.createElement('script'), s = d.scripts[0];
wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js';
s.parentNode.insertBefore(wf, s);
})(document);
</script>
The Web Font Loader does most of the JavaScript work for you, providing callbacks and setting body classes for each font variation. Note: fvd is a font variation description.
WebFontConfig = {
loading: function() {},
active: function() {},
inactive: function() {},
fontloading: function(familyName, fvd) {},
fontactive: function(familyName, fvd) {},
fontinactive: function(familyName, fvd) {}
};
.wf-loading
.wf-active
.wf-inactive
.wf-<familyname>-<fvd>-loading
.wf-<familyname>-<fvd>-active
.wf-<familyname>-<fvd>-inactive
Font Face Observer, 4KB minified
Font Face Observer is a great, lightweight option from Bram Stein. Bram works at Typekit and contributes to the Web Font Loader, so I’m just going to quote him here:
“Font Face Observer is a small @font-face loader and monitor compatible with any web font service. It will monitor when a web font is applied to the page and notify you. It does not limit you in any way in where, when, or how you load your web fonts. Unlike the Web Font Loader, Font Face Observer uses scroll events to detect font loads efficiently and with minimum overhead.”
For more on how it works, see http://bramstein.com/writing/detecting-system-fonts-without-flash.html.
var observer = new FontFaceObserver('My Family', {
weight: 400
});
observer.check().then(function () {
console.log('Font is available');
}, function () {
console.log('Font is not available');
});
Font Face Observer can do anything the Web Font Loader can do if you don’t mind writing some additional JavaScript. For example, here’s a quick script you might use for a single callback of several font families:
var fontFamilies = {
'Open Sans': [
{
weight: 400
},
{
weight: 700
}
],
'Open Sans Condensed': [
{
weight: 700
}
]
}
var fontObservers = [];
Object.keys(fontFamilies).forEach(function(family) {
fontObservers.push(fontFamilies[family].map(function(config) {
return new FontFaceObserver(family, config).check()
}));
});
Promise.all(fontObservers)
.then(function() {
document.body.classList.add('fonts-loaded');
}, function() {
console.log('Fonts not available');
});
Check out an example in a CodePen.
LocalFont
LocalFont is a free service that makes it easy to load fonts and retrieve them from localStorage. The website converts your font files to base64 and generates the CSS and JavaScript you need to get going. This technique is definitely fast, but I have not been able to find any definitive data on its performance compared to browser cache (see https://github.com/jaicab/localFont/issues/1). You may also need to devise a cache-busting strategy for any assets updates. Ultimately, I would not advise using localStorage unless you have the time and resources to test thoroughly.
Embracing FOUT
In order for these font loading strategies to work, you’ll need to plan on starting page render with local fonts. But the switch from a fallback to a downloaded web font doesn’t have to be so bad! A couple of suggestions that may help:
- Use a resource like http://fontfamily.io/ to choose a fallback font based on what’s available.
- Minimize reflow by customizing the fallback font’s size, line-height, etc. to closely resemble the web font(s). This will minimize the flash of unstyled text during the switch, particularly on text-heavy pages. Also coming soon: http://www.stateofwebtype.com/#font-size-adjust
- Set a timeout for applying the custom font at all. If the font is still loading after a certain amount of time, you may want to simply skip it to avoid a jarring switch.
- After your web font has been cached, subsequent page views can bypass the font loading detection with a cookie or similar technique. However, on most modern device and browser combinations, the cache or local storage retrieval of the font file(s) is so fast that you may not need to worry about this.
Font Loading in the future
It's clear that there is some room for improvement here. Luckily, with the Font Loading API and the CSS Font Rendering draft, the future will provide new and improved font loading control.
Font Loading API
The new Font Loading API listens to @font-face
natively to remove the need for existing JavaScript loaders. This is already available in Chrome and Opera! Bram Stein also has a Font Loading polyfill available on GitHub.
var f = new FontFace("newfont", "url(newfont.woff)", {});
f.load().then(function (loadedFace) {
document.fonts.add(loadedFace);
document.body.style.fontFamily = "newfont, serif";
});
document.fonts.ready().then(function() {
// all @font-face families in use are ready
});
http://caniuse.com/#feat=font-loading
CSS Font Rendering Draft
CSS Font Rendering will allow developers to control browser font rendering behaviour with only CSS. It’s currently part of the Fonts Level 4 specification, but it’s still too early to tell when this will be available.
@font-face {
font-display: auto | block | swap | fallback | optional;
}
Wrap up
Standard @font-face
can hurt site performance, but there are many ways to load web fonts without blocking text render. And working around these problems is getting eaiser.
Ultimately, I hope that this post has you thinking about font loading on your sites. Go to webpagetest.org to check the page load filmstrips for your sites. See what tweaking @font-face
can do for you, and most importantly, your users!