Web Fonts and Bandwidth Privilege


by
2063w

We’ve given websites a lot of crap over the year for such things as

In many cases, the excuses pile up, and some challenges seem more insurmountable than others. Publishers need a revenue model, people need to know who’s visiting the website and reading the article, and non-technical editors need to build some website at one point, and might be afraid of stepping into the minefield of an outsourced code base.

But then there’s this:

Before font load: invisible text. Before font load: text is finally visible.
Flash of invisible text during load compared to the finished result.

That right here is seconds of staring of absolutely nothing on a website, because someone decided they wanted non-standard fonts, either because you had to pry them from the cold dead hands of the webdesigner, or because the typeface was considered important for #branding. (People generally don’t talk about the #branding of a website that loads execrably.)

What you see is the so-called Flash of Invisible Text (FOIT).

Most websites have this problem—it is a very common experience, because most people have got it in their head that they need to use fancy web fonts for their website.

The good news is that it’s easy to fix.

The bad news is that this means you don’t have a lot of excuses for not fixing it.

Background

As part of a collaboration with /r/india to track the promises of the Modi administration, I first created my own mock-up in Jekyll—which was later spun off into its own project.

Given that the target demographic was largely Indian, my concept was informed by optimization as the main priority.

I was largely successful in this, and one of the accomplishments—amongst others, detailed in the GitHub issue—was bringing the size of the entire page below 100 kB.

When a project gets that small, everything starts to stand out. It caught my attention that my Raleway fonts took up more than two thirds of the entire page, which felt ridiculous to me.

I’ve grown very fond of custom web fonts, since I started using the Skeleton CSS framework, which powers the majority of this site, and many others as well.

The Dark Side of Web Fonts

But being obsessed with optimization, I always have to justify the use of it to myself. And for a project aimed at a country with varying speeds of bandwidth—compounded by the fact that one the world’s biggest CDNs only just opened data centres in a country of 1.25 billion people1 after we began on the project—playing fast and loose with fancy fonts demanded a justification.

Because two really bad things happen when you use a custom web font:

  1. Loading the font delays (blocks) the loading of the rest of the website (DOM).2
  2. Your browser renders the website, regardless of whether the font has finished loading, which creates a Flash Of Invisible Text (FOIT).

This is preventable, if developers want to do something about it. What follows is a description of how I improved the loading of web fonts on Goal Tracker, and what the results were. I’ll skip to the results first, so I don’t lose you with the boring code samples.

Your mileage may vary, but testing on a slow-ish 3G connection, I got the following results:

> Timelapse of loading with FOIT. Only the final frame has a readable web page at 3.5s
The unoptimized web page with FOIT.
> Timelapse of loading unoptimized web page with FOIT. The web page is readable from 1.3s and renders the page with the web font at 2.3s
Timelapse of loading optimized web page without FOIT.

Here they are as animated GIFs; you have to click them first, though.

This shows an animated timelapse of loading with FOIT. Only the final frame has a readable web page at 3.5s
The unoptimized web page with FOIT has 5 frames, because the site is repainted every time each of the three web fonts are loaded.
This shows an animated timelapse of loading unoptimized web page with FOIT. The web page is readable from 1.3s and renders the page with the web font at 2.3s.
The optimized web page without FOIT has 3 frames, 2 fewer, because the site is is only repainted once all three web fonts have been loaded.

Here are the main takeaways from the improvement:

  • The website first renders (ie DOM finished)
    • in 2.5s on the unoptimized version
    • in 1.3s on the optimized version
  • The website is readable
    • in 3.5s on the unoptimized version
    • in 1.3s on the optimized version—with fallback fonts
    • in 2.3s on the optimized version—with the web fonts

Again, this is for a web page with a minimal size footprint, so make of it what you will. If you can shave off a second for a website to render and two seconds for it to be readable even on a web page smaller than 100 kB, then any website will benefit.

The only “downside” is that users will see a Flash Of Unstyled Text (FOUT), a smaller—and briefer—inconvenience compared to a FOIT.

How-To

The following how-to is from my personal HTML guide for web fonts. I have abridged the code snippets from the guide to make them more readable. Some asides also wound up on the cutting-room floor.

Defeating FOIT

Change your stylesheet from including the custom font that looks like so:

body { font-family: "Raleway", Georgia, sans-serif; }

To this:

- body { font-family: "Raleway", Georgia, sans-serif; }
+ body { font-family: Georgia, sans-serif; }
+ .fonts-loaded body { font-family: "Raleway", Georgia, sans-serif; }

At the bottom of the HTML body, add the following using Font Face Observer:

<script src="/static/js/fontfaceobserver.js"></script>
<script>
    var raleway300 = new FontFaceObserver("Raleway", {"weight": 300}),
        raleway400 = new FontFaceObserver("Raleway", {"weight": 400}),
        raleway600 = new FontFaceObserver("Raleway", {"weight": 600});

    Promise.all([
        raleway300.check(),
        raleway400.check(),
        raleway600.check()
    ]).then(function() {
        document.documentElement.className += "fonts-loaded";
    });
</script>

This defeats FOIT by loading the fallback fonts first and then applying the custom fonts, once, and only once, they’ve finished downloading.

Deferred Font Loading

External

Either retrieve web fonts from Google Fonts:

<!-- Preconnect to external font to speed up font loading -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Delay download of font until document is loaded -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:300,400,600" media="deferred" onload="if(media!='all')media='all'" />
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:300,400,600" media="all" /></noscript>
Local

Or load them locally using localfont.com.

<!-- Delay download of font until document is loaded -->
<link rel="stylesheet" href="/static/typefaces/css/fonts.css" media="deferred" onload="if(media!='all')media='all'" />
<noscript><link rel="stylesheet" href="/static/typefaces/css/fonts.css" media="all" /></noscript>

Where fonts.css looks something like this:

@font-face {
    font-family: "Raleway";
    font-weight: 300;
    font-style: normal;
    src: url("../fonts/Raleway-300/Raleway-300.eot");
    src: url("../fonts/Raleway-300/Raleway-300.eot?#iefix") format("embedded-opentype"),
         local("Raleway Light"),
         local("Raleway-300"),
         url("../fonts/Raleway-300/Raleway-300.woff2") format("woff2"),
         url("../fonts/Raleway-300/Raleway-300.woff") format("woff"),
         url("../fonts/Raleway-300/Raleway-300.ttf") format("truetype"),
         url("../fonts/Raleway-300/Raleway-300.svg#Raleway") format("svg");
}

@font-face {
    font-family: "Raleway";
    font-weight: 400;
    font-style: normal;
    src: url("../fonts/Raleway-regular/Raleway-regular.eot");
    src: url("../fonts/Raleway-regular/Raleway-regular.eot?#iefix") format("embedded-opentype"),
         local("Raleway"),
         local("Raleway-regular"),
         url("../fonts/Raleway-regular/Raleway-regular.woff2") format("woff2"),
         url("../fonts/Raleway-regular/Raleway-regular.woff") format("woff"),
         url("../fonts/Raleway-regular/Raleway-regular.ttf") format("truetype"),
         url("../fonts/Raleway-regular/Raleway-regular.svg#Raleway") format("svg");
}

@font-face {
    font-family: "Raleway";
    font-weight: 600;
    font-style: normal;
    src: url("../fonts/Raleway-600/Raleway-600.eot");
    src: url("../fonts/Raleway-600/Raleway-600.eot?#iefix") format("embedded-opentype"),
         local("Raleway SemiBold"),
         local("Raleway-600"),
         url("../fonts/Raleway-600/Raleway-600.woff2") format("woff2"),
         url("../fonts/Raleway-600/Raleway-600.woff") format("woff"),
         url("../fonts/Raleway-600/Raleway-600.ttf") format("truetype"),
         url("../fonts/Raleway-600/Raleway-600.svg#Raleway") format("svg");
}

/** I prefer to place this part in fonts.css, but you can place it in your regular style.css */
.fonts-loaded body { font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; }

Backward Compatibility

Deferred Font Loading
  • This method does not work in Android < 4.4 because the onload handler does not fire when content is available—I’m looking into a workaround for this.
  • Some browsers appear to still block CSS render despite media="none". This means CSS loads as it usually does—I’m looking into this.

Keith Clark: “Loading CSS without Blocking Render”

Deferred Font Rendering (FOIT)

FontFaceObserver has been tested and works on the following browsers:

  • Chrome (desktop & Android)
  • Opera
  • Safari (desktop & iOS)
  • IE9+
  • Android WebKit

Font Face Observer browser support section

You could say that this might not work on literally every browser, with the worst case scenario being that the page just loads as it normally would. You would also have to compare this browser compatibility list with the tech stack you’re currently using, which is probably not all-inclusive either.

Further Reading

  1. You can see where you are receiving this article from under loc in this CDN trace↩︎

  2. The ills of this are alleviated by HTTP/2, which was just implemented by CloudFlare. ↩︎