Extremely fast loading with Gatsby and self-hosted fonts

Last update: 18 October, 2019
Table of contents

In this post, you’ll see how to improve the First Contentful Paint (FCP) and the First Meaningful Paint (FMP) of your Gatsby application. The “trick” is to self-host your fonts. When you do that, you can reduce the loading of your application often by 1 second on 3G connections. To make the self-hosting easier, you will use the typefaces package. Let’s now see why you can achieve those results if you change the font-loading strategy.

If you prefer, you can skip the explanation and the project setup, and go to the required steps. You can also check the updates section because things changed (not much) since I wrote this post.

Problem

When you choose some fonts from the Google fonts library, the recommended way to load them is to either add a link tag in the head of your HTML:

<link
  href="https://fonts.googleapis.com/css?family=Lato"
  rel="stylesheet"
/>

or use the @import rule inside your CSS:

@import url("https://fonts.googleapis.com/css?family=Lato");

After that, you can use the fonts by referencing them in your CSS:

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

When we specify the link or the @import rule, what we download first is a small CSS file with the @font-face definitions for our fonts. For example:

/* skipping stuff... */
/* latin */
@font-face {
  font-family: "Open Sans";
  font-style: normal;
  font-weight: 400;
  src: local("Open Sans Regular"), local("OpenSans-Regular"),
    url(https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVZ0bf8pkAg.woff2)
      format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: "Playfair Display";
  font-style: normal;
  font-weight: 400;
  src: local("Playfair Display Regular"), local(
      "PlayfairDisplay-Regular"
    ),
    url(https://fonts.gstatic.com/s/playfairdisplay/v13/nuFiD-vYSZviVYUb_rj3ij__anPXDTjYgEM86xRbPQ.woff2)
      format("woff2");
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* skipping stuff... */

This file contains instructions on where to find the required fonts in various formats. There are many formats (in my case there is only woff2—I’m using the latest version of Chrome—but they can be more) because not all devices or browsers use the same file types.

The problem here is that this CSS file has the highest possible download priority. That means that the browser will stop the initial page render until this CSS file has finished downloading. This, by the way, happens for all the external CSS files.

But that’s not the only problem.

After that, the browser will start downloading the actual fonts. The browser knows what fonts to download after parsing our CSS and rendering some HTML elements that use the fonts. Again here, the font requests have the highest priority and also block the page render.

Solution

A common solution to this problem is to self-host your Google fonts (or any fonts). This way, you can skip the initial CSS download, and start downloading the font files instead. If the fonts are not too many, or we don’t have a huge JavaScript file to download in parallel, our FCP and FMP can be drastically improved. To prove this, we’ll set up a basic Gatsby project.

If you don’t want to set up a new project, check this repo. You can switch branches to test the different solutions for yourself.

Setting up a new Gatsby project.

We’ll use the gatsby-cli to create a new project with the default starter:

gatsby new self-host-fonts
cd self-host-fonts
code .

I’m using styled-components to add the fonts, but you can use plain CSS if you want. You’ll have to install 3 separate packages for styled-components to work:

yarn add gatsby-plugin-styled-components styled-components babel-plugin-styled-components

After that’s done, add gatsby-plugin-styled-components in your gatsby-config.js:

gatsby-config.js
module.exports = {
  siteMetadata: {
    // site metadata
  },
  plugins: [
    // other plugins...
    `gatsby-plugin-styled-components`,
  ],
};

You’ll use the Playfair Display font for the headings and Open Sans for the rest of the content. You’ll first use those fonts with the recommended method from Google fonts, and then, you’ll measure the loading time.

First, delete the src/components/layout.css to make the CSS rules simpler. After that, add a global style in your src/components/layout.js file:

src/components/layout.js
import React from "react";
import PropTypes from "prop-types";
import { StaticQuery, graphql } from "gatsby";
import { createGlobalStyle } from "styled-components";

import Header from "./header";

const GlobalStyle = createGlobalStyle`
  @import url("https://fonts.googleapis.com/css?family=Open+Sans|Playfair+Display:400,700");

  * {
    box-sizing: border-box;
  }
  body {
    margin: 0;
    font-family: 'Open Sans', sans-serif;
    font-size: 18px;
  }

  h1, h2, h3, h4, h5, h6 {
    font-family: 'Playfair Display', serif;
  }
`;

const Layout = ({ children }) => (
  <StaticQuery
    query={graphql`
      query SiteTitleQuery {
        site {
          siteMetadata {
            title
          }
        }
      }
    `}
    render={(data) => (
      <>
        <GlobalStyle />
        <Header siteTitle={data.site.siteMetadata.title} />
        <div
          style={{
            margin: `0 auto`,
            maxWidth: 960,
            padding: `0px 1.0875rem 1.45rem`,
            paddingTop: 0,
          }}
        >
          {children}
          <footer>
            © {new Date().getFullYear()}, Built with
            {` `}
            <a href="https://www.gatsbyjs.org">Gatsby</a>
          </footer>
        </div>
      </>
    )}
  />
);

Layout.propTypes = {
  children: PropTypes.node.isRequired,
};

export default Layout;

And that’s all you need for this method to work.

Let’s test the loading time with Lighthouse. You’ll have to build the application and deploy it on Netlify—or to your preferred service. Sadly, I can’t see any benefits when I serve it locally with a tool like serve. I won’t give instructions on how to setup Netlify, but don’t worry, it’s well documented.

Now, build the app, and deploy it on Netlify with the following commands:

gatsby build && netlify deploy --prod --dir=public

Open a new Incognito window on Chrome, and enter the URL. Then, open the developer tools, and navigate to the Audits tab. Finally, press the blue button that says Run audits. The results in my case were the following:

default results
default results

1.6s for both FCP and FMP. That’s actually really good (thanks Gatsby), but there’s room for improvement.

The results are a bit different when I measure with the Performance tab. Before every test, I clear the data and measure in 3G. The FMP happens at about 2.1s. If I add the &display=swap, the FMP goes down to 1.6s (see the updates section for context).

Let’s see now how you can improve that score by self-hosting the same fonts with the typefaces package.

Self-hosting fonts with typefaces package

First, install the packages for Playfair Display and Open Sans typefaces. If you go to the packages section on GitHub, you can find both of them. Install them with:

yarn add typeface-playfair-display typeface-open-sans

Then import them in your gatsby-browser.js file:

gatsby-browser.js
require("typeface-playfair-display");
require("typeface-open-sans");

Don’t forget to remove the @import from the <GLobalStyle> component in layout.js.

Now, build again the application, deploy it, and test it with Lighthouse. Those were my results:

self-hosting results
self-hosting results

There is a 0.8 seconds improvement for both FCP and FMP! That’s a big improvement for the cost of installing 2 packages and writing 2 lines of code.

If you inspect the resulting HTML page by pressing Ctrl + U in Windows, you see that the first <style> tag, high up in the head of the page, contains the font faces and uses the font-display property:

@font-face {
  /* ... */
  font-display: swap;
  /* ... */
}

The font-display: swap property tells the browser the following. If the fonts are not available in the initial page render, use the fallback font to allow the page to render. In that scenario, we see the Flash of Unstyled Content (FOUP). When the fonts finish downloading, the browser will replace them with the actual fonts.

Preload fonts

Consider adding preload links for your fonts to start downloading them before the browser needs them. This will not improve the first paint metrics, but it may decrease the time the browser displays the fallback fonts (FOUT). A Gatsby plugin that can help with that is gatsby-plugin-preload-fonts. You should test first to see if preloading the fonts improves the font-loading for your site before implementing it.

Final words

Let’s summarize some key points:

  • Self-hosting fonts improves the first paint metrics because you don’t download the CSS file that has the font faces.
  • font-display: swap eliminates the flash of invisible text by displaying fallback fonts. This should improve the first meaningful paint—if displaying text is considered meaningful for your app. I say that because sometimes in the performance tab of Chrome DevTools you see the FMP happening while the text is invisible—something that seems wrong to me.

If you want to know more about font loading, an expert on this subject is Zach Leatherman. If you want something quick, you can watch this video from DevTips.

Updates

  • Google now allows you to use font-display: swap; when requesting a Google Font. This gives a small boost, but it’s still slower than self-hosting. For example, you can say:

    @import url("https://fonts.googleapis.com/css?family=Noto+Sans+HK&display=swap");

    In fact, this is the default option when you copy the embed code from their website.

  • The typefaces library supports only the Latin subset. You can’t use it for other languages. My (dirty) workaround, in this case, is the following: First, I customize the Google Font request. Then, I visit the generated URL, copy all the font faces, and place them in a CSS file. Finally, I import that file in the layout.js. The performance would be the same as if you were using the typefaces library. You can check the code in the copy-css branch of the repo.

    If you don’t trust the URLs in the font faces, you can download the fonts from the Google Fonts app (“download this selection” button), self-host them, and change the remote URLs to the local URLs. In other words, you do the same thing the typefaces package does.

Other things to read

Popular

Previous/Next