The Best Font Loading Strategies and How to Execute Them

Avatar of Zell Liew
Zell Liew on

Zach Leatherman wrote up a comprehensive list of font loading strategies that have been widely shared in the web development field. I took a look at this list before, but got so scared (and confused), that I decided not to do anything at all. I don’t know how to begin loading fonts the best way and I don’t want to feel like an idiot going through this list.

Today, I gave myself time to sit down and figure it out. In this article, I want to share with you the best loading strategies and how to execute all of them.

The best strategies

Zach recommends two strategies in his article:

  1. FOUT with Class – Best approach for most situations. (This works whether we use a font-hosting company or hosting our own fonts.)
  2. Critical FOFT – Most performant approach. (This only works if we host our own fonts.)

Before we dive into these two strategies, we need to clear up the acronyms so you understand what Zach is saying.

FOIT, FOUT, FOFT

The acronyms are as follows:

  • FOIT means flash of invisible text. When web fonts are loading, we hide text so users don’t see anything. We only show the text when web fonts are loaded.
  • FOUT means flash of unstyled text. When web fonts are loading, we show users a system font. When the web font gets loaded, we change the text back to the desired web font.
  • FOFT means flash of faux text. This one is complicated and warrants more explanation. We’ll talk about it in detail when we hit the FOFT section.

Self-hosted fonts vs. cloud-hosted fonts

There are two main ways to host fonts:

  1. Use a cloud provider.
  2. Self-host the fonts.

How we load fonts differs greatly depending on which option you choose.

Loading fonts with cloud-hosted fonts

It’s often easier to cloud-hosted fonts. The provider will give us a link to load the fonts. We can simply plunk this link into our HTML and we’ll get our web font. Here’s an example where we load web fonts from Adobe Fonts now (previously known as Typekit).

<link rel="stylesheet" href="https://use.typekit.net/your-kit-id.css">

Unfortunately, this isn’t the best approach. The href blocks the browser. If loading the web font hangs, users won’t be able to do anything while they wait.

Typekit used to provide JavaScript that loads a font asynchronously. It’s a pity they don’t show this JavaScript version anymore. (The code still works though, but I have no idea when, or if, it will stop working.)

Loading fonts from Google Fonts is slightly better because it provides font-display: swap. Here’s an example where we load Lato from Google Fonts. (The display=swap parameter triggers font-display: swap).

<link
  href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&display=swap"
  rel="stylesheet"
/>

Loading fonts with self-hosted fonts

You can only self-host your fonts if you have the license to do so. Since fonts can be expensive, most people choose to use a cloud provider instead.

There’s a cheap way to get fonts though. Design Cuts runs font deals occasionally where you can get insanely high-quality fonts for just $29 per bundle. Each bundle can contain up to 12 fonts. I managed to get classics like Claredon, DIN, Futura, and a whole slew of fonts I can play around by camping on their newsletter for these deals.

If we want to self-host fonts, we need to understand two more concepts: @font-face and font-display: swap.

@font-face

@font-face lets us declare fonts in CSS. If we want to self-host fonts, we need to use @font-face to declare your fonts.

In this declaration, we can specify four things:

  • font-family: This tells CSS (and JavaScript) the name of our font.
  • src: Path to find the font so they can get loaded
  • font-weight: The font weight. Defaults to 400 if omitted.
  • font-style: Whether to italicize the font. Defaults to normal if omitted.

For src, we need to specify several font formats. For a practical level of browser support, we can use woff2 and woff.

Here’s an example where we load Lato via @font-face.

@font-face {
  font-family: Lato;
  src: url('path-to-lato.woff2') format('woff2'),
       url('path-to-lato.woff') format('woff');
}

@font-face {
  font-family: Lato;
  src: url('path-to-lato-bold.woff2') format('woff2'),
       url('path-to-lato-bold.woff') format('woff');
  font-weight: 700;
}

@font-face {
  font-family: Lato;
  src: url('path-to-lato-italic.woff2') format('woff2'),
       url('path-to-lato-italic.woff') format('woff');
  font-style: italic;
}

@font-face {
  font-family: Lato;
  src: url('path-to-lato-bold-italic.woff2') format('woff2'),
       url('path-to-lato-bold-italic.woff') format('woff');
  font-weight: 700;
  font-style: italic;
}

If you don’t have woff2 or woff files, you can upload your font files (either Opentype or Truetype) into a font-face generator to get them.

Next, we declare the web font in a font-family property.

html {
  font-family: Lato, sans-serif;
}

When browsers parse an element with the web font, they trigger a download for the web font.

font-display: swap

font-display takes one of four possible values: auto, swap, fallback, and optional. swap instructs browsers to display the fallback text before web fonts get loaded. In other words, swap triggers FOUT for self-hosted fonts. Find out about other values from in the CSS-Tricks almanac.

Browser support for font-display: swap is pretty good nowadays so we can use it in our projects.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
6058No7911.1

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12212312211.3-11.4

FOUT vs. FOUT with Class

FOUT means flash of unstyled text. You always want FOUT over FOIT (flash of invisible text) because it’s better for users to read words written with system fonts compared to words written with invisible ink. We mentioned earlier that font-display: swap gives you the ability to use FOUT natively.

FOUT with Class gives you the same results—FOUT—but it uses JavaScript to load the fonts. The mechanics are as follows:

  • First: Load system fonts.
  • Second: Load web fonts via JavaScript.
  • Third: When web fonts are loaded, add a class to the <html> tag.
  • Fourth: Switch the system font with the loaded web font.

Zach recommends loading fonts via JavaScript even though font-display: swap enjoys good browser support. In other words, Zach recommends FOUT with Class over @font-face + font-display: swap.

He recommends FOUT with Class because of these three reasons:

  1. We can group repaints.
  2. We can adapt to user preferences.
  3. We can skip font-loading altogether if users have a slow connection.

I’ll let you dive deeper into the reasons in another article. While writing this article, I found a fourth reason to prefer FOUT with Class: We can skip font-loading when users already have the font loaded in their system. (We do this with sessionStorage as we’ll see below.)

FOUT with Class (for cloud-hosted fonts)

First, we want to load our fonts as usual from your cloud-hosting company. Here’s an example where I loaded Lato from Google Fonts:

<head>
  <link
    href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&display=swap"
    rel="stylesheet"
  />
</head>

Next, we want to load fonts via JavaScript. We’ll inject a script into the <head> section since the code footprint is small, and it’s going to be asynchronous anyway.

<head>
  <link
    href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&display=swap"
    rel="stylesheet"
  />
  <script src="js/load-fonts.js"></script>
</head>

In load-fonts.js, we want to use the CSS Font Loading API to load the font. Here, we can use Promise.all to load all fonts simultaneously. When we do this, we’re grouping repaints.

The code looks like this:

if ('fonts' in document) {
  Promise.all([
    document.fonts.load('1em Lato'),
    document.fonts.load('700 1em Lato'),
    document.fonts.load('italic 1em Lato'),
    document.fonts.load('italic 700 1em Lato')
  ]).then(_ => () {
    document.documentElement.classList.add('fonts-loaded')
  })
}

When fonts are loaded, we want to change the body text to the web font. We can do this via CSS by using the .fonts-loaded class.

/* System font before [[webfont]]s get loaded */
body {
  font-family: sans-serif;
}

/* Use [[webfont]] when [[webfont]] gets loaded*/
.fonts-loaded body {
  font-family: Lato,  sans-serif;
}

Pay attention here: We need to use the .fonts-loaded class with this approach.

We cannot write the web font directly in the <body>‘s font-family; doing this will trigger fonts to download, which means you’re using @font-face + font-display: swap. It also means the JavaScript is redundant.

/* DO NOT DO THIS */
body {
  font-family: Lato, sans-serif;
}

If users visit additional pages on the site, they already have the fonts loaded in their browser. We can skip the font-loading process (to speed things up) by using sessionStorage.

if (sessionStorage.fontsLoaded) {
  document.documentElement.classList.add('fonts-loaded')
} else {
  if ('fonts' in document) {
  Promise.all([
    document.fonts.load('1em Lato'),
    document.fonts.load('700 1em Lato'),
    document.fonts.load('italic 1em Lato'),
    document.fonts.load('italic 700 1em Lato')
  ]).then(_ => () {
    document.documentElement.classList.add('fonts-loaded')
  })
  }
}

If we want to optimize the JavaScript for readability, we can use an early return pattern to reduce indentation.

function loadFonts () {
  if (sessionStorage.fontsLoaded) {
    document.documentElement.classList.add('fonts-loaded')
    return 
  } 
  
if ('fonts' in document) {
  Promise.all([
    document.fonts.load('1em Lato'),
    document.fonts.load('700 1em Lato'),
    document.fonts.load('italic 1em Lato'),
    document.fonts.load('italic 700 1em Lato')
  ]).then(_ => () {
    document.documentElement.classList.add('fonts-loaded')
  })  
}

loadFonts()

FOUT with Class (for self-hosted fonts)

First, we want to load our fonts as usual via @font-face. The font-display: swap property is optional since we’re loading fonts via JavaScript.

@font-face {
  font-family: Lato;
  src: url('path-to-lato.woff2') format('woff2'),
       url('path-to-lato.woff') format('woff');
}

/* Other @font-face declarations */

Next, we load the web fonts via JavaScript.

if ('fonts' in document) {
  Promise.all([
    document.fonts.load('1em Lato'),
    document.fonts.load('700 1em Lato'),
    document.fonts.load('italic 1em Lato'),
    document.fonts.load('italic 700 1em Lato')
  ]).then(_ => () {
    document.documentElement.classList.add('fonts-loaded')
  })
}

Then we want to change the body text to the web font via CSS:

/* System font before webfont is loaded */
body {
  font-family: sans-serif;
}

/* Use webfont when it loads */
.fonts-loaded body {
  font-family: Lato,  sans-serif;
}

Finally, we skip font loading for repeat visits.

if ('fonts' in document) {
  Promise.all([
    document.fonts.load('1em Lato'),
    document.fonts.load('700 1em Lato'),
    document.fonts.load('italic 1em Lato'),
    document.fonts.load('italic 700 1em Lato')
  ]).then(_ => () {
    document.documentElement.classList.add('fonts-loaded')
  })
}

CSS Font Loader API vs. FontFaceObserver

The CSS Font Loader API has pretty good browser support, but it’s a pretty cranky API.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
3541No7910

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12212312210.0-10.2

So, if you need to support older browsers (like IE 11 and below), or if you find the API weird and unwieldy, you might want to use Bramstein’s FontFaceObserver. It’s super lightweight so there’s not much harm.

The code looks like this. (It’s much nicer compared to the CSS Font Loader API).

new FontFaceObserver('lato')
  .load()
  .then(_ => {
    document.documentElement.classList.add('fonts-loaded')
  })

Make sure to use fontfaceobserver.standalone.js if you intend to it to load fonts on browsers that don’t support Promises.

FOFT

FOFT means flash of faux text. The idea here is we split font loading into three stages:

  • Step 1: Use fallback font when web fonts aren’t loaded yet.
  • Step 2: Load the Roman (also called “book” or “regular”) version of the font first. This replaces most of the text. Bold and italics will be faked by the browser (hence “faux text”).
  • Step 3: Load the rest of the fonts

Note: Zach also calls this Standard FOFT.

Using Standard FOFT

First, we load the Roman font.

@font-face {
  font-family: LatoInitial;
  src: url('path-to-lato.woff2') format('woff2'),
       url('path-to-lato.woff') format('woff');
  unicode-range: U+65-90, U+97-122;
}

.fonts-loaded-1 body {
  font-family: LatoInitial;
}
if('fonts' in document) {
  document.fonts.load("1em LatoInitial")
    .then(_ => {
      document.documentElement.classList.add('fonts-loaded-1')
    })
}

Then, we load other fonts.

Pay attention here: we’re loading Lato again, but this time, we set font-family to Lato instead of LatoInitial.

/* Load this first */
@font-face {
  font-family: LatoInitial;
  src: url('path-to-lato.woff2') format('woff2'),
       url('path-to-lato.woff') format('woff');
  unicode-range: U+65-90, U+97-122;
}

/* Load these afterwards */
@font-face {
  font-family: Lato;
  src: url('path-to-lato.woff2') format('woff2'),
       url('path-to-lato.woff') format('woff');
  unicode-range: U+65-90, U+97-122;
}

/* Other @font-face for different weights and styles*/

.fonts-loaded-1 body {
  font-family: LatoInitial;
}

.fonts-loaded-2 body {
  font-family: Lato;
}
if ('fonts' in document) {
  document.fonts.load('1em LatoInitial')
    .then(_ => {
      document.documentElement.classList.add('fonts-loaded-1')

      Promise.all([
        document.fonts.load('400 1em Lato'),
        document.fonts.load('700 1em Lato'),
        document.fonts.load('italic 1em Lato'),
        document.fonts.load('italic 700 1em Lato')
      ]).then(_ => {
        document.documentElement.classList.add('fonts-loaded-2')
      })
    })
}

Again, we can optimize it for repeat views.

Here, we can add fonts-loaded-2 to the <html> straightaway since fonts are already loaded.

function loadFonts () {
  if (sessionStorage.fontsLoaded) {
    document.documentElement.classList.add('fonts-loaded-2')
    return
  }

  if ('fonts' in document) {
    document.fonts.load('1em Lato')
      .then(_ => {
        document.documentElement.classList.add('fonts-loaded-1')

        Promise.all([
          document.fonts.load('400 1em Lato'),
          document.fonts.load('700 1em Lato'),
          document.fonts.load('italic 1em Lato'),
          document.fonts.load('italic 700 1em Lato')
        ]).then(_ => {
          document.documentElement.classList.add('fonts-loaded-2')

          // Optimization for Repeat Views
          sessionStorage.fontsLoaded = true
        })
      })
  }
}

Critical FOFT

The “critical” part comes from ‘critical css” (I believe) – where we load only essential CSS before loading the rest. We do this because it improves performance.
When it comes to typography, the only critical things are letters A to Z (both capitals and small letters). We can create a subset of these fonts with unicode-range.

When we create this subset, we can also create a separate font file with the necessary characters.

Here’s what @font-face declaration looks like:

@font-face {
  font-family: LatoSubset;
  src: url('path-to-optimized-lato.woff2') format('woff2'),
       url('path-to-optimized-lato.woff') format('woff');
  unicode-range: U+65-90, U+97-122;
}

We load this subset first.

.fonts-loaded-1 body {
  font-family: LatoSubset;
}
if('fonts' in document) {
  document.fonts.load('1em LatoSubset')
    .then(_ => {
      document.documentElement.classList.add('fonts-loaded-1')
    })
}

And we load other font files later.

.fonts-loaded-2 body {
  font-family: Lato;
}
if ('fonts' in document) {
  document.fonts.load('1em LatoSubset')
    .then(_ => {
      document.documentElement.classList.add('fonts-loaded-1')

      Promise.all([
        document.fonts.load('400 1em Lato'),
        document.fonts.load('700 1em Lato'),
        document.fonts.load('italic 1em Lato'),
        document.fonts.load('italic 700 1em Lato')
      ]).then(_ => {
        document.documentElement.classList.add('fonts-loaded-2')
      })
    })
}

Again, we can optimize it for repeat views:

function loadFonts () {
  if (sessionStorage.fontsLoaded) {
    document.documentElement.classList.add('fonts-loaded-2')
    return
  }

  if ('fonts' in document) {
    document.fonts.load('1em LatoSubset')
      .then(_ => {
        document.documentElement.classList.add('fonts-loaded-1')

        Promise.all([
          document.fonts.load('400 1em Lato'),
          document.fonts.load('700 1em Lato'),
          document.fonts.load('italic 1em Lato'),
          document.fonts.load('italic 700 1em Lato')
        ]).then(_ => {
          document.documentElement.classList.add('fonts-loaded-2')

          // Optimization for Repeat Views
          sessionStorage.fontsLoaded = true
        })
      })
  }
}

Critical FOFT variants

Zach proposed two additional Critical FOFT variants:

  • Critical FOFT with Data URI
  • Critical FOFT with Preload

Critical FOFT with data URIs

In this variant, we load the critical font (the subsetted roman font) with data URIs instead of a normal link. Here’s what it looks like. (And it’s scary.)

@font-face {
  font-family: LatoSubset;
  src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff");
  /* ... */
}

This code looks scary, but there’s no need to be scared. The hardest part here is generating the data URI, and CSS-Tricks has us covered here.

Critical FOFT with preload

In this variant, we add a link with the preload tag to the critical font file. Here’s what it looks like:

<link rel="preload" href="path-to-roman-subset.woff2" as="font" type="font/woff2" crossorigin>

We should only preload one font here. If we load more than one, we can adversely affect the initial load time.

In the strategy list, Zach also mentioned he prefers using a data URI over the preload variant. He only prefers it because browser support for preload used to be bad. Today, I feel that browser support is decent enough to choose preloading over a data URI.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
5085No7911.1

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12212312211.3-11.4

Final note from Zach

Chris ran this article via Zach and Zach wished he prioritized a JavaScript-free approach in his original article.

I think the article is good but I think my article that its based off of is probably a little dated in a few ways. I wish it prioritized no-JS approaches more when you’re only using one or two font files (or more but only 1 or 2 of each typeface). The JS approaches are kind of the exception nowadays I think (unless you’re using a lot of font files or a cloud provider that doesn’t support font-display: swap)

This switches the verdict a tiny bit, which I’m going to summarize in the next section.

Which font loading strategy to use?

If you use a cloud-hosted provider:

  • Use font-display: swap if the host provides it.
  • Otherwise, use FOUT with class

If you host your web fonts, you have a few choices:

  1. @font-face + font-display: swap
  2. FOUT with Class
  3. Standard FOFT
  4. Critical FOFT

Here’s how to choose between them:

  • Choose @font-face + font-display: swap if you’re starting out and don’t want to mess with JavaScript. It’s the simplest of them all. Also choose this option if you use only few font files (fewer than two files) for each typeface.
  • Choose Standard FOFT if you’re ready to use JavaScript, but don’t want to do the extra work of subsetting the Roman font.
  • Choose a Critical FOFT variant if you want to go all the way for performance.

That’s it! I hope you found all of this useful!

If you loved this article, you may like other articles I wrote. Consider subscribing to my newsletter 😉. Also, feel free to reach out to me if you have questions. I’ll try my best to help!