Over a million developers have joined DZone.

Optimal Font Loading With Accessibility

In this excerpt from author Jeremy Wagner's new book "Web Optimization in Action," you'll learn about the visual anomalies that can occur when fonts load, how to use the Font Loading API to control how fonts display, and how to fall back to a JavaScript solution when the Font Loading API isn't available.

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

This article was excerpted from the Chapter 7 of Web Optimization in Action, an upcoming book from Manning Publications by Jeremy Wagner. Follow Jeremy on Twitter @malchata.

Loading any asset for a website involves pitfalls that vary based on their type. For example, loading CSS with the <link> tag blocks rendering until the stylesheet is downloaded, parsed and the styles are applied to the document. <script> tags that reference external JavaScript files similarly block rendering of the page when they are placed toward the top of the document.

Fonts are no different, and the loading of them causes no shortage of vexing problems that we may be tasked by our client to solve. Because fonts are loaded within CSS referenced by <link> tags, there is some render blocking that’s inherent to the process. Fonts, however, introduce their own challenges. In this article, you'll learn about the visual anomalies that can occur when fonts load, how to use the Font Loading API to control how fonts display, and how to fall back to a JavaScript solution when the Font Loading API isn't available.

Understanding Font Loading Problems

You have a client who’s sent you an email, and they appreciate the work you've done on their new website. However, they're noticing that text seems to take a little while to appear on the page on a slower mobile connection.

This is an understandable concern, but the reality is that this is just how browsers work when it comes to downloading fonts. The phenomenon the client is referring to is called the Flash of Invisible Text.

Flash of Invisible Text (henceforth referred to as FOIT) is similar to how the Flash of Unstyled Content (FOUC) anomaly manifests, only instead of unstyled content, we're dealing with a scenario where text is invisible until the document's fonts are loaded. It's noticeable on even fast connections if one pays close attention, but as connection speed decreases and network latency increases, the effect becomes evident. Mobile devices on aging mobile networks such as 2G and 3G are more susceptible to this phenomenon. This problem can be seen in Figure 1.

Image titleFigure 1 - As a page loads embedded fonts, the text is initially invisible (left) until fonts load, at which point the text styled in those font faces begins to appear (right.)

Figure 1 - As a page loads embedded fonts, the text is initially invisible (left) until fonts load, at which point the text styled in those font faces begins to appear (right.)

This may seem like an annoying bug, but it's how browsers are designed to behave. The reason why browsers wait to render text while downloading fonts is to avoid an effect known as the Flash of Unstyled Text (FOUT). The browser will only hide text for so long while a font is loading, though, and when this period of time is exceeded, the unstyled text shows up before the font has finished downloaded. Once the font has downloaded, the unstyled text is then styled. This is shown in Figure 2.

Image title

Figure 2 - When the wait time for a font resource becomes too long, the text will suddenly become visible, but is unstyled because of the still-loading font resource (right). Once all of the fonts load, then the text will become styled (right.) This is known as the Flash of Unstyled Text (FOUT.)

The browser's intentions are good, but the problem is that if a connection stalls, the user could be left waiting for 3 seconds or longer to read text on the page if they're using Chrome, Opera or Firefox. In other browsers like Safari and Mobile Safari, the content just doesn't show if the request hasn't been completed. If the user aborts loading the page or the font resources otherwise fail to load, the content may never show. This holds true even if the developer of the website has specified fallbacks to system fonts in their font-family properties.

So what to do? We embrace the flash of unstyled text on page load and use the Font Loading API to load fonts and apply them to our page styles when they've been loaded. Upon navigation to subsequent pages, the fonts will be in the browser cache, and then we’ll fall back to the browser’s built-in rendering behavior. This approach ensures that your content will appear as soon as possible, won't leave your users in a lurch with hidden text. Let's get started!

Using the Font Loading API

The Font Loading API is a JavaScript-based tool that we can use to control the display of fonts as they load. The open-ended nature of the API gives us a lot of latitude to determine how to apply fonts to a document, whether they're hosted on your own server or with a font provider such as Google Fonts.

Before we get started, we'll need to use git to download an interactive example in which you can follow along, and use Node to run a local webserver. To do this, go into your command line and type the following:

git clone https://github.com/webopt/ch7-fonts.git
cd ch7-fonts
git checkout -f font-loader-api
npm install
node http.js

This will install all of the Node dependencies for the exercise and run a small static file server at http://localhost:8080. If you don’t want to use Node, you can also navigate to the folder in your browser. If you don’t want to use the git client, you can download the code from https://github.com/webopt/ch7-fonts as a zip file as well.

Quick note: If you get stuck or just want to skip ahead to see the completed font loading API code looks and behaves, you can do so by entering this command in your terminal:

git checkout -f font-loading-api-complete

Now you're ready to dig in!

Controlling How and When Fonts Display

To get started, open up the developer tools in Google Chrome, change your network throttling profile to "Regular 3G,” and you'll be able to see the FOIT effect pretty easily. Generally speaking, the slower the connection, the more noticeable the effect is. To pinpoint the moment in time when fonts become visible on the screen, you can toggle the "Capture Screenshots" button in the network tab as shown in Figure 3.

Image title

Figure 3 - The toggle button to capture screenshots in the Chrome developer tools.

When this button is toggled and the page is reloaded, a roll of screenshots of the page loading will populate above the request waterfall graph. Using this, we can pinpoint the exact moment in time that fonts appear on the page. With the "Regular 3G" throttling profile selected, the text doesn't appear until about 875 milliseconds from the time the page begins to download. This amount of time is okay, but depending on connection speed and latency, it could fluctuate. The goal is to allow the user to see content as soon as possible.

To do this, we need to start looking in styles.css for what font-family definitions there are that use embedded fonts. For this site, we have three different font variants: Open Sans Light, Open Sans Regular and Open Sans Bold. Table 1 is a listing of these font-family definitions and the selectors they apply to.

font-familyAssociated CSS Selectors
"Open Sans Light".navItem a
"Open Sans Regular"body
"Open Sans Bold".articleTitle
Table 1 - A listing of the embedded fonts' font-family property values and their associated CSS selectors.

We can use this information to do two things. The first thing is to replace the font-family properties for all of these with system fonts. For this website, we'll use the following property and value for the associated selectors in Table 1:

font-family: "Helvetica", "Arial", sans-serif;

This removes the Open Sans font families from the page, which allows the content to be seen immediately since these fonts are not downloaded from the web server.

Secondly, we’ll take these selectors place them under a class named fonts-loaded that we'll put on the <html> element when the fonts have been loaded by the web browser. Before we write the font loading script, however, we'll write the CSS for how the Open Sans fonts will be applied once this class is applied to the <html> element:

.fonts-loaded body{
    font-family: "Open Sans Regular";

.fonts-loaded .navItem a{
    font-family: "Open Sans Light";

.fonts-loaded .articleTitle,
.fonts-loaded .sectionHeader{
    font-family: "Open Sans Bold";

By placing this small snippet of CSS at the end of styles.css, we can control when we apply the font faces we're loading with the font loading API. Because we've specified the system fonts as the initial font set, the "unstyled" text will be immediately visible when the page begins to load, and the fonts will be applied once they've loaded.

Open index.html in your text editor to get started with writing the font loading script. After the <link> tag that imports styles.css (which imports our fonts,) add this code inside of a <script> tag:

  if(document.fonts){ //#A
    document.fonts.load("1em Open Sans Light"); //#B
    document.fonts.load("1em Open Sans Regular"); //#B
    document.fonts.load("1em Open Sans Bold"); //#B

    document.fonts.ready.then(function(fontFaceSet){ //#C
      document.documentElement.className += " fonts-loaded"; //#D
    document.documentElement.className += " fonts-loaded"; //#E
#A Checks for the presence of the font loading API.
#B The Font Loading API uses the load() method to load the fonts.
#C The ready.then method is a JavaScript promise that executes when all of the fonts specified have loaded.
#D If all the fonts for the document have loaded, the fonts-loaded class will be applied to the <html> element.
#E If the font loading API is unavailable in the user's browser, just apply the fonts-loaded class to the <html> document and load the fonts normally.

The core property of the font loading API that we're using here is the font object's load method. Rather than using the API to explicitly load a font by its URL, we rely on the CSS to define @font-faces for the document. Just because the @font-faces are defined, however, doesn't mean that the browser downloads those fonts. Browser downloading behavior is quite optimized, and all modern browsers will inspect the document to see if any of the @font-faces are in use. If they are, they'll be downloaded, but since we initially set all of our font-family values to use system fonts, none of the font variants are downloaded until we tell the browser to do so via the load() method.

When all of the fonts have loaded, the fonts-loaded class is added to the <html> element. This defeats the browser's initial Flash of Invisible Text, allowing the content to be read as soon as the document is loaded and the CSS is applied. Then the fonts are applied to the document when they're available by adding the fonts-loaded class to the <html> element. This method ensures that no matter what may happen on the user's end, the text will be visible as soon as possible.

One drawback of this method is that it does cause a repainting of text elements on the page, but the increased accessibility is worth the trade-off. If you choose system fonts that are similar to the embedded fonts, document reflow can be minimized.

Optimizing for Repeat Visitors

Our solution works great for the user that's coming to our site for the first time, but we need to optimize for repeat visits when the fonts are already in the user's browser cache. With the code as it is now, the Flash of Unstyled Text occurs on subsequent page visits even when the font is cached. We can overcome this by using a cookie and modifying our font loading code.

All we need to do is modify two parts of the code we've written. On the line where we check for the font loading API, we add an additional condition to check for the presence of a cookie:

if(document.fonts && document.cookie.indexOf("fonts-loaded") !== -1){

This modification adds an additional check for a cookie named fonts-loaded, and has no corresponding value. We check for its presence by using the indexOf string method, which returns (somewhat unintuitively) a value of -1 if the search string is not found. This ensures that the font loading code we've written only runs if the font loading API is available and a fonts-loaded cookie hasn't been set.

But now we need to actually set that cookie somewhere. To do that, we add this bit of code after the line where we add the fonts-loaded class to the <html> element:

document.cookie = "fonts-loaded=";

This adds an empty cookie by the name of fonts-loaded for the current domain. When this cookie is set and the user navigates to subsequent pages, the font loading code doesn't run again, and the else condition takes effect and immediately adds the fonts-loaded class to the <html> element.

This does reintroduce the Flash of Invisible Text effect, but the risks of the effect are mitigated because the fonts are in the user's browser cache. FOIT is okay so long as we can be assured that the fonts will load. Now that the fonts are in the cache, the assurance can be made that the FOIT effect will never block the user from seeing the content of the page.

JavaScript is a fine way to check the cookie's value and apply the fonts-loaded class accordingly. However, if you really want to be speedy about it, you could use a back end language like PHP to modify the document so that the fonts-loaded class is on the <html> element when the content is sent by the server. You would just remove the else condition that adds the class in the JavaScript code and modify the output by checking the cookie on the server end. The code below shows how this would be done in PHP:

<?php if(isset($_COOKIE["fonts-loaded"])){ //#A
    ?><html class="fonts-loaded"> <!-- #B -->
<?php }
else {
    ?><html> <!-- #C -->
<?php } ?>
#A Checks to see if the fonts-loaded cookie is set via the isset() function.
#B If the cookie is set, add the fonts-loaded class on the <html> element.
#C If the cookie is not set, do not modify the <html> element.

By using a server-side language to craft the response, we're modifying the <html> element faster since the JavaScript has to be downloaded and interpreted by the browser before it can modify the <html> element to trigger the retrieval of the fonts from the cache. That said, the JavaScript solution is also quite fast, so both are reasonable solutions. It just depends on what tools you have at your disposal, your skill set, and the time available to you.

Accommodating Users With JavaScript Disabled

As always, it comes back to the users who have JavaScript disabled. Because of the way we've developed this solution, the @font-faces we've specified will never take effect for JavaScript-disabled users because the font loading scripts never run and apply the fonts-loaded class to the document. The result is that the content will be displayed using only the system fonts.

If you or your organization doesn't care that this small segment of users will never get to see your fancy new fonts in action, then feel free to call it a day. If, on the other hand, this is a problem, we should go over a quick fix that involves our old friend the <noscript> tag.

We can use <noscript> to default to the old loading behavior by nesting an inline <style> tag that applies the Open Sans font families as the default:

<noscript> <!-- #A -->
    <style> /* #B */
            font-family: "Open Sans Regular", "Helvetica", "Arial", sans-serif;

        .navItem a{
            font-family: "Open Sans Light", "Helvetica", "Arial", sans-serif;

            font-family: "Open Sans Bold", "Helvetica", "Arial", sans-serif;
</noscript> <!-- #A -->
#A <noscript> block that buckets styles for users with JavaScript disabled.
#B Inline CSS that assigns fonts to elements without respect to the fonts-loaded class.

By adding this small piece of inline CSS, we're returning the user without JavaScript to the native font loading experience. This means that while they won't be able to reap the benefits of the Font Loading API, they will be provided with the base level of functionality.

To cap things off, we'll explore using the Font Face Observer library to polyfill what the Font Loading API provides, ensuring the widest possible browser support.

Using Font Face Observer as a Fallback

The unfortunate reality is that the Font Loader API isn't univerally supported. It has strong support in Chrome and Firefox, but browsers such as Safari, Internet Explorer and Edge are lacking. Our client would appreciate it if we could make sure that more browsers receive an optimal font loading experience. This is where a polyfill such as Font Face Observer comes in.

Font Face Observer is a font loading library by Danish developer Bram Stein. While it is not a direct polyfill in the sense that you can drop it into a page and have your existing Font Loading API code work without a hitch, it gives the developer similar ability to manage font loading.

In this short section, we'll write a script that falls back when the font loading API is not available and loads two external scripts: The Font Face Observer script, and a script that loads the fonts via Font Face Observer. To get started, you'll need to download some new code from Github. To do this, type in git checkout -f fontface-observer and once the code has downloaded, you'll be ready to start!

Conditionally Loading the External Scripts

Once you've downloaded the code from Github, you'll notice a js folder with two scripts: fontfaceobserver.min.js, which is the minified Font Face Observer library, and fontloading.js, which contains an empty closure where we'll place the alternate font loading behavior. Because we don't want to invoke the overhead of the Font Face Observer script, we only want to load it and the script file with our fallback loading behavior when the font loading API isn't available. To do this, we add the following code between the initial if conditional that checks for the document.fonts object and the fonts-loaded cookie, and the else conditional that follows it.

else if(!document.fonts && document.cookie.indexOf("fonts-loaded") === -1){ //#A
    var fontFaceObserverScript = document.createElement("script"), //#B
        fontLoadingScript = document.createElement("script"); //#B

    fontFaceObserverScript.src = "js/fontfaceobserver.min.js"; //#C
    fontLoadingScript.src = "js/fontloading.js"; //#C
    fontFaceObserverScript.defer = "defer"; //#D
    fontLoadingScript.defer = "defer"; //#D

    document.head.appendChild(fontFaceObserverScript); //#E
    document.head.appendChild(fontLoadingScript); //#E
#A Checks to see if the font loading API is unavailable and if the fonts-loaded cookie has not been set.
#B Creates new <script> elements for both the Font Face Observer and font loading scripts.
#C Sets the src attributes for the locations for both scripts.
#D Sets the defer attribute to defer parsing of the script until after the DOM is loaded so that rendering is not blocked.
#E Appends the <script> elements to the end of the <head> tag, effectively loading these scripts.

The above code is very simple. If the font loading API isn't available and the fonts-loaded cookie has not been set, we then create new <script> elements for both the Font Face Observer and font loading scripts and set their src attributes to their respective locations. To ensure they don't block page rendering, we set the defer attribute for both. Once we've set everything up, we instruct the browser to load these scripts by appending them to the end of the <head> element.

Writing the Font Loading Behavior

Open up js/fontloading.js in your text editor and you'll notice that the contents of this file is just an empty JavaScript closure. Starting at line 2, add this code:

document.onreadystatechange = function(){ //#A
    var openSansLight = new FontFaceObserver("Open Sans Light"), //#B
        openSansRegular = new FontFaceObserver("Open Sans Regular"), //#B
        openSansBold = new FontFaceObserver("Open Sans Bold"); //#B

    Promise.all([openSansLight.check(), openSansRegular.check(), openSansBold.check()]).then(function(){ //#C
        document.documentElement.className += " fonts-loaded"; //#D
        document.cookie = "fonts-loaded="; //#E
#A Waits for the DOM to be ready (similar to jQuery's $(document).ready() method.)
#B Specifies the font sources to be used in this document.
#C A Promise that waits for all of the fonts to be loaded.
#D If all fonts have loaded, the fonts-loaded class is appended to the document, rendering the downloaded fonts.
#E The fonts-loaded cookie is set so that subsequent page loads will load the font from the cache and avoid the Flash of Unstyled Text.

Font Face Observer's syntax is similar to that of the font loading API, but with slight differences. We define a FontFaceObserver object for each font variant we want to load. Then, through a JavaScript promise, we wait until all fonts have loaded. Once the fonts have loaded, we can then apply the fonts-loaded class to the <html> element, and set the fonts-loaded cookie. This allows us to reuse the mechanism by which we control the display of our fonts that we used in the font loading API.

The result of this effort is an effective and widely compatible method that uses a native API where available, but then falls back to a capable polyfill. With this code in place, our client is happy with their font rendering, and as you know, a happy client is the only kind that you want.

To learn more about optimizing the loading of web fonts and other web optimization topics, check out Web Optimization in Action at https://jeremywagner.me/book and save 39% with discount code wagnerdz.

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

web performance,web development,front end,fonts,performance problems,accessibility,web optimization,javascript,css,web fonts

Published at DZone with permission of Jeremy Wagner. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}