Extremely fast loading with Gatsby and self-hosted fonts

Last update: June 27, 2019

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 here 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 from Kyle Mathews.

If you want, 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.

Let’s now see why you can achieve those results if you change the font-loading strategy.

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:

<style>
@import url('https://fonts.googleapis.com/css?family=Lato');
</style>

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 our case is only woff2, 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. Again here, the actual font requests have the highest priority and also block the page render.

Usually, what happens in practice is the following. The First Contentful Paint is delayed by the CSS file that contains the @font-faces and the JavaScript. The First Meaningful Paint is delayed by the actual font downloads.

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 loading the required fonts immediately. 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: {
    title: `Gatsby Default Starter`,
    description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
    author: `@gatsbyjs`
  },
  plugins: [
    `gatsby-plugin-react-helmet`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/src/images`
      }
    },
    `gatsby-transformer-sharp`,
    `gatsby-plugin-sharp`,
    `gatsby-plugin-styled-components`,    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `gatsby-starter-default`,
        short_name: `starter`,
        start_url: `/`,
        background_color: `#663399`,
        theme_color: `#663399`,
        display: `minimal-ui`,
        icon: `src/images/gatsby-icon.png` // This path is relative to the root of the site.
      }
    }
    // this (optional) plugin enables Progressive Web App + Offline functionality
    // To learn more, visit: https://gatsby.app/offline
    // 'gatsby-plugin-offline',
  ]
};

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. Sadly, I can’t see any benefits when I serve it locally with a tool like serve. So you’ll have to host it on Netlify or to your preferred service. 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. They are not bad, but there’s room for improvement.

Note: The results are 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 typefaces for Playfair Display and Open Sans fonts. 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
/**
 * Implement Gatsby's Browser APIs in this file.
 *
 * See: https://www.gatsbyjs.org/docs/browser-apis/
 */

// You can delete this file if you're not using it
require("typeface-playfair-display");
require("typeface-open-sans");

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

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

self-hosting results
self-hosting results

As you can see, there is a 0.8 seconds improvement for both FCP and FMP!

That’s a ridiculous improvement for the cost of installing 2 packages and writing 2 lines of code.

If you inspect the resulting HTML page by pressing Cntl + U in Windows, you see that the first <style> tag, high up in the head of the page, contains the font-faces.

The second style tag, before you download any other file, is the actual CSS. That means that the browser will start downloading the fonts pretty much immediately. So there’s no need for adding a preload link as a hint.

Another key aspect is that if you inspect the font-faces, you’ll see the following line:

@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 (in our case they are), 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.

If you want to know more about font loading, an expert on this subject is Zach Leatherman. If you don’t have the time to spare and 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 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 have questions/suggestions, you can leave a comment below, or tag me at Twitter.

Other things to read

Popular notes

Other posts