Font Loading Strategies - Web Fonts Performance - Responsive Web Design, Part 2 (2015)

Responsive Web Design, Part 2 (2015)

Web Fonts Performance

Font Loading Strategies

You’ve squeezed the most out of your font formats, set the correct caching headers, and generated the smallest possible subsets. Now it’s time to load fonts efficiently by applying a font loading strategy suitable to your performance and design requirements.

APPLYING FONTS SELECTIVELY

A simple approach to improving web font performance is to only use web fonts selectively. Remember that a web font is loaded on demand; it isn’t loaded if there is no CSS selector (using that web font) that matches an element in the DOM. This makes media queries a great tool for responsive typography. A good example is the website for A List Apart, which uses media queries to apply a condensed version of their typeface on smaller viewports26.

Implementing the same behavior is straightforward. Start by setting the default web font and use media queries to apply a different font stack when the viewport matches your desired dimensions. If the media query doesn’t match, the condensed font won’t be downloaded, and the regular font will be used.

@font-face {

font-family: My Font;

src: url(myfont.woff);

}

@font-face {

font-family: My Font Condensed;

src: url(myfont-condensed.woff);

}

body {

font-family: My Font, Arial, sans-serif;

}

@media (max-width: 400px) {

body {

font-family: My Font Condensed, Arial, sans-serif;

}

}

Media queries let you selectively use custom font stacks. They should not, however, be used to enable or disable web fonts based on device pixel ratios or viewport dimensions. With ever increasing resolutions on modern handheld devices, your media queries might get executed even on devices that are often on unreliable and slow cellular connections. Likewise, mobile devices with small viewports may be on high-speed connections (such as Wi-Fi). Screen sizes or device pixel ratios don’t say much about whether or not the device’s internet connection is fast (which is the deciding factor for loading fonts or not). Use media queries as a way to load fonts based on your design and not simply to reduce page load.

Another example of applying fonts selectively is Type Rendering Mix27, which adds classes based on your browser’s text rendering engine and the antialiasing method.

<html class="tr-coretext tr-aa-subpixel">

Using these classes you could switch to a different font-weight based on the text rendering engine your visitors are using. For example, Mac OS X’s Core Text renders text slightly heavier than Microsoft’s DirectWrite text rendering engine. You could solve this discrepancy by applying a lighter weight of your font in case your visitor is using Mac OS X with subpixel antialiasing.

html {

font-family: My Font, Arial, sans-serif;

font-weight: 400;

}

html.tr-coretext {

font-weight: 300;

}

These are just two examples of this very useful technique. Using JavaScript and classes you could enable, disable or change your fonts based on any condition.

SIMULATING SWAPPING AND BLOCKING BEHAVIOR

If you prefer the swapping behavior (as used in Internet Explorer), it is possible to simulate it in browsers that do not have native support. We do this by creating a helper function that returns a promise and resolves after a given amount of time.

function timeout(ms) {

return new Promise(function (resolve, reject) {

setTimeout(reject, ms);

});

}

We can use this promise to race against the font loading promises. If the font loading promise resolves before the timeout, we know the fonts have loaded. If instead the timeout fires, the promise will be rejected and we can display the fallback font.

First, we add a class to the <html> element to indicate fonts have started loading. Later we can use this to style against.

var html = document.documentElement;

html.classList.add('fonts-loading');

Next, we create an instance of FontFace for each font we want to load and add them to the document’s FontFaceSet.

var myfont = new FontFace('My Font', 'url(myfont.woff)');

document.fonts.add(myfont);

We then start the race between the font loading and the timeout. If the promise is resolved, the fonts-loading class is replaced by the fonts-active class, and if the promise is rejected (because of the timeout), it is replaced by the fonts-inactive class.

Promise.race([

document.fonts.load('16px My Font'),

timeout(3000)

]).then(function () {

html.classList.remove('fonts-loading');

html.classList.add('fonts-active');

}, function () {

html.classList.remove('fonts-loading');

html.classList.add('fonts-inactive');

});

We can use these three classes to style the page and to simulate swapping and blocking behavior. The first step is to always apply the fallback font to the page. This will ensure that the browser won’t wait for web fonts to load and will display the text as soon as possible.

html {

font-family: Arial, sans-serif;

}

Once the fonts load, the fonts-active class will be added to the <html> element. We can use this to apply the web font.

html.fonts-active {

font-family: My Font, Arial, sans-serif;

}

Because the browser has already loaded the font it will not trigger the browser’s default font loading behavior and so applies the font immediately. If the font fails to load, nothing happens because the fallback font will remain active.

We can use the same three classes to simulate blocking behavior in all browsers. Again, we use the fallback font for all content, but instead of showing the text immediately we hide the content while the fonts-loading class is on the <html> element. The reason we hide the text on the fonts-loading class and not the html selector itself is to prevent the page from being invisible on clients that do not support JavaScript.

html {

font-family: Arial, sans-serif;

}

html.fonts-loading {

visibility: hidden;

}

Like before, if the fonts load, the fonts-active class will be added to the <html> element. We use this to add the font to the font stack and make the content visible.

html.fonts-active {

font-family: My Font, Arial, sans-serif;

visibility: visible;

}

If the timeout triggers, the content will still be invisible. To fix that, we use the fonts-inactive class to reset the visibility.

html.fonts-inactive {

visibility: visible;

}

With both methods, it is possible for the fonts to load after the timeout has triggered. This won’t actually affect your page in either case because the web font will never be applied to your page unless the fonts-active class is set. Remember, a timeout is important to reduce the reflow that happens after a user is already engaged in reading the content on the page.

ASYNCHRONOUS LOADING AND CACHING

If your primary goal is to have your website’s content visible as soon as possible — regardless of whether it is using web fonts or a fallback font — you might consider asynchronously loading fonts and explicitly managing the cache. This technique was used by the Guardian newspaper to optimize its front-end performance. The approach, as described in Smashing Magazine’s performance case study28, is to explicitly cache fonts in localStorage after they have been retrieved for the first time. On the first request, the fonts are loaded through an AJAX request. Once the fonts have finished downloading, they are stored in localStorage and also inserted into the DOM. On the next request, the fonts can be retrieved from localStorage and inserted into the DOM right away instead of making another network request.

This approach to asynchronous loading and explicit caching is fairly easy to implement. To start, we’ll create a reusable method to insert the content of a style sheet in the document. We’ll use this method to insert the style sheet after it has been retrieved from the server or from localStorage.

function insertStylesheet(css) {

var style = document.createElement(style);

style.textContent = css;

document.head.appendChild(style);

}

The following logic is executed on every page request and checks if the style sheet containing the fonts is present in localStorage. This check should preferably be included in the <head> section of the page in order to prevent the browser from applying its default font loading behavior. If the style sheet with the fonts is stored in localStorage, it is directly inserted into the page using the insertStylesheet method. If there is no entry for the fonts in localStorage, it will make an AJAX request for it. The result of that request will be stored in localStorage and then also displayed.

if (localStorage.fontStylesheet) {

insertStylesheet(localStorage.fontStylesheet);

} else {

var request = new XMLHttpRequest();

request.open('GET', 'myfonts.css');

request.onload = function () {

if (request.status === 200) {

var stylesheet = request.responseText;

localStorage.fontStylesheet = stylesheet;

insertStylesheet(stylesheet);

}

};

request.send();

}

A variation of this technique does not render the fonts after retrieving them; instead, the first request always uses the fallback fonts, and first-time visitors do not experience swapping behavior. Subsequent requests will then find the font in localStorage and render the fonts immediately. In essence, this is the “optional” behavior as described in the CSS font-rendering proposal. You can do this by removing the insertStylesheet call after storing the font in localStorage.

The most commonly cited reason for using this technique is to optimize both the initial load from a clean cache (by loading the fonts asynchronously), and the load time for frequent visitors. While this may seem like a good approach on paper, it doesn’t buy you much in practice. The main benefit of this approach is that it takes fonts off the critical path by loading them asynchronously. However, this benefit isn’t unique to this method, and many other font loading techniques are asynchronous.

A problem with this approach is that it requires fonts to be inlined in the style sheet to be efficient. While this avoids multiple requests, it also comes with all the downsides of inlining fonts that were discussed in the “Inlining Fonts” section above. Tests have also shown that localStorage is slightly slower than reading from the browser’s cache though this overhead is insignificant compared to parsing a font and rerendering the page.

The caching benefits of storing fonts in localStorage for frequent visitors are also uncertain. Visitors who could benefit from this approach are your most frequent visitors. They are also the most likely to have many of the assets of your website already present in their browser cache. Treating fonts differently from other resources takes up extra storage in the browser (or device), most likely violates your font licence, and forces you to recreate caching behavior that is already well implemented and understood: the browser cache.

A much better approach is to load fonts asynchronously, and as soon as possible by using the browser’s cache. This is easily achieved using the native Font Load API (or the polyfill29). The following code creates a new FontFaceinstance, and then preloads it. During loading, the font is not available to the document so the fallback font will be shown immediately. If the font loads successfully we add it to the document by calling document.fonts.add. This will make it available to the page and trigger a rerender of the fallback font to the desired web font.

var myfont = new FontFace('My Font', 'url(myfont.woff)');

myfont.load().then(function () {

document.fonts.add(myfont);

});

The nice thing about this approach is that it transparently reuses the browser’s cache. On the first load, the fallback font will be used until the font is retrieved from the network and stored in the browser’s cache. On subsequent requests the font is already cached, after which the load() method returns immediately. This in turn causes the font to be added to the page immediately.

The downside of this approach is that the fonts are always swapped in when they load. This is desirable early in the page load cycle but becomes distracting for your users once the fallback font has been shown for a while. A better approach would be to show the font if it loads within a certain period of time and continue to show the fallback font if it fails to load during that time. On subsequent requests, the font will be cached and thus shown immediately. We can do this by reusing the timeout function we created earlier.

function timeout(ms) {

return new Promise(function (resolve, reject) {

setTimeout(reject, ms);

});

}

As before, we create a new FontFace instance for our font. Instead of calling then directly, we use the Promise.race method together with the timeout function. In this case, we race the font load promise against the timeout promise with a value of three seconds.

var myfont = new FontFace('My Font', 'url(myfont.woff)');

Promise.race([

myfont.load(),

timeout(3000)

]).then(function () {

document.fonts.add(myfont);

});

The race promise could settled by either the font loading or by the timeout being triggered. The promise will only be resolved if the font has loaded within the timeout. In this case, we add the font to the document and make it available for the page. If the promise is rejected, nothing happens, and the fallback font will continue to be used. On subsequent page loads, the browser will go through the same process again, but this time the font is already cached and can be loaded and rendered quickly.

It is also possible to combine this approach with your preferred font rendering approach. By default, this method uses a fallback font until the web font is swapped in, but by adding loading, active and inactive classes you can simulate any font loading approach.

var html = document.documentElement;

html.classList.add('fonts-loading');

var myfont = new FontFace('My Font', 'url(myfont.woff)');

myfont.load().then(function () {

document.fonts.add(myfont);

html.classList.remove('fonts-loading');

html.classList.add('fonts-active');

}).catch(function () {

html.classList.remove('fonts-loading');

html.classList.add('fonts-inactive');

});

If you prefer blocking behavior, you can hide content while fonts are loading, and show it again if your fonts are loaded (or fail to load, an equally important case to cover).

html {

font-family: My Font, Arial, sans-serif;

}

html.fonts-loading, html.fonts-inactive {

visibility: hidden;

}

html.fonts-active {

visibility: visible;

}

This approach is very flexible. It lets you simulate any loading strategy with very little code and takes advantage of the browser’s cache for the best performance. This approach isn’t limited to a single font either, as the next section on prioritized loading will show you.

PRIORITIZED LOADING

For sites that need to load a lot of fonts, there’s another strategy that works really well. The basic idea is to divide the fonts into several groups with different priorities. The first group should contain fonts that are used more often on the site and are necessary to render the most important content. The next font groups contain fonts of decreasing importance.

We can do this by waiting for the first group to load and only then start loading the second group, and so forth. If we have four fonts to load, but only the first and second are the primary ones, we could load them in two groups (1 and 2, and 3 and 4).

var font1 = new FontFace('Font1', 'url(font1.woff)'),

font2 = new FontFace('Font2', 'url(font2.woff)'),

font3 = new FontFace('Font3', 'url(font3.woff)'),

font4 = new FontFace('Font4', 'url(font4.woff)');

Like before, we use the promise all method to create a group consisting of multiple fonts. When all fonts in that group have loaded, we add them to the document and start loading the second group.

Promise.all([

font1.load(),

font2.load()

]).then(function () {

document.fonts.add(font1);

document.fonts.add(font2);

Promise.all([

font3.load(),

font4.load()

]).then(function () {

document.fonts.add(font3);

document.fonts.add(font4);

});

});

The benefit of this approach is that it forces the browser to prioritize the first group (because it doesn’t yet know about the second group). This prevents cases where, for example, fonts from the second group load before the fonts in the first group. With a larger number of fonts, it also prevents blocking the browser’s download queue which has a limited number of connections per host. The failure mode of this approach is straightforward owing to the default behavior of the promise all method. If one or multiple fonts in a group fail to load, the promise will be rejected, and none of the subsequent groups will be loaded. This avoids cases where a partial group or lower-priority group is loaded before the group with the highest priority.

Prioritizing can also be used to reduce the distracting reflow caused by swapping. By loading only the regular style of a font in the first group, the browser will generate faux bold and italic variations while downloading the second group containing the real bold and italic variations. Because the browser is using the regular variation to generate the faux ones, the reflow caused by loading the true variations will be much less distracting than waiting for all variations (including regular) to load all at once. This technique has been described in Zach Leatherman’s article, “Flash of Faux Text — still more on Font Loading30”.

This is surprisingly easy to implement use the Font Load API. First, load the regular style and add it to the document. Once it has been added, trigger downloading the other variation(s) of the same font (in this case, bold). Since we added the regular style to the document before loading the bold variation, the browser will swap in the font and generate a faux bold style until the real bold has loaded.

var myfontRegular = new FontFace('My Font', 'url(myfont-regular.woff)', {

weight: 400

}),

myFontBold = new FontFace('My Font', 'url(myfont-bold.woff)', {

weight: 700

});

myFontRegular.load().then(function () {

document.fonts.add(myFontRegular);

myFontBold.load().then(function () {

document.fonts.add(myFontBold);

});

});

Another example of prioritizing is to load complementary subsets or supersets of a single font. A subset without any special characters and no OpenType features loads much faster than a superset of the same font with all characters and OpenType features. You can exploit this by loading the minimal subset first and using it to show your content as soon as possible. You can then asynchronously load the superset and insert it into the page with the same name. Reflow will still happen, depending on which OpenType features are included in the superset, but this should be minimal considering it is a superset of the same font.

var myfont = new FontFace('My Font', 'url(myfont.woff)');

myfont.load().then(function () {

document.fonts.add(myfont);

var fontAllChars = new FontFace('My Font', 'url(myfont-all-chars.woff)');

fontAllChars.load().then(function () {

document.fonts.add(fontAllChars);

});

});

It is possible to vary loading and rendering strategies per group as well. For example, with carefully chosen timeout values and assuming a small primary group, you could consider adopting blocking behavior for the primary group while subsequent groups use fallback fonts until the web fonts are swapped in.