Extremely fast loading with Gatsby and self-hosted fonts
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.
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
:
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:
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:
1.6s for both FCP and FMP. That’s actually really good (thanks Gatsby), but there’s room for improvement.
&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:
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:
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
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts