Optimizing Web Fonts in Next.js 13

December 7, 2022

Web fonts are an essential aspect of modern web design. They allow for beautiful typography on the web, unique to your product✨

However, using web fonts can also introduce challenges. If you're a frequent internet user like most people reading this, chances are that you've come cross the following phenomenon:

You navigate to a website that initially displays a default font, such as Times New Roman or Arial, until the web font has been downloaded and installed. It then quickly switches the font from the default to the web font, often causing layout shift.

The layout shift can range from just moving a couple of pixels to a major shift caused by large fonts, or by different line break positions.

This phenomenon is called Flash Of Unstyled Text (FOUT). If the web font takes a long time to download, the user may see the text on the page "flash" or change from the default font to the web font.

In other cases, you may not see any text until the web font has been downloaded

This is called Flash Of Invisible Text (FOIT), in which case the browser draws an invisible fallback font. This happens when the font-display CSS property is set to block.

I think we can all agree that both FOUT and FOIT are... distracting and confusing. However, it's challenging to avoid FOUT and FOIT entirely.

One of the main challenges with using web fonts is that the browser must download the typeface before it can be rendered and displayed on the page. This can take time, especially if the user has a slow internet connection or if the web font is large.

Luckily, there are some optimizations we can implement to make working with web fonts a little less painful!


Optimizations

Before we cover the optimizations, let's first take a look at the most basic, unoptimized approach to understand the issues we're dealing with.

In the following examples, we will be showing the Next.js implementation when using web fonts. However, it is important to note that these examples and techniques are not specific to Next.js and can also be applied when using other frameworks or just HTML.


HTTP Connections

To add a web font to a Next.js project, you can use a <link> tag in the <head> of the _document.js file. In the_document.js file, you can modify the Document component and pass the font stylesheet as children to the Head component.

import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <link href="https://fonts.googleapis.com/css2?family=Nunito+Sans&display=swap" rel="stylesheet" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

When we load a page using the above-mentioned code as the content of _document.js , the browser goes through several steps to make sure the web font is properly loaded and displayed on the page.

  1. The browser starts by reading the initial HTML and encounters the <link> tag, which it identifies as an external stylesheet.

  2. The HTML parser then checks the rel attribute to figure out what type of resource the link is referring to. In this case, it's a CSS stylesheet.

  3. Next, the HTML parser sends an HTTP request to the server at the specified URL, fonts.googleapis.com, and receives a response containing the CSS rules and styles for the web font, in this case looking like the following:

/* latin */
@font-face {
  font-family: "Nunito Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/nunitosans/v12/....woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, ...;
}

For readability, I only included the latin subset here. In reality, more subsets are included in the response.

  1. After the stylesheet is downloaded synchronously, the browser constructs the rendering tree using the DOM and CSSOM. This initial rendering tree shows the fallback font to the user.

  2. Once the rendering tree is fully constructed and the first paint has occurred, the browser sends a request to fonts.gstatic.com to download the actual font file specified in the @font-face's src property.

  3. Finally, the browser uses the font file to render the font on the page, allowing the web font to be displayed correctly.

If we take a closer look at both the stylesheet and font request, you'll see that it's not just about downloading the resource. Because we're contacting other servers, the browser first has to set up a connection using DNS lookups, TLS negotiations, and TCP handshakes.

Establishing a connection can take quite a bit of the total request time. To avoid this at request time, we can add two <link> tags using the preconnect browser hint for both https://fonts.googleapis.com and https://fonts.gstatic.com.

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
        <link
          href="https://fonts.googleapis.com/css2?family=Nunito+Sans&display=swap"
          rel="stylesheet"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

This tag will instruct the browser to establish a connection to the server where the resource is hosted as soon as the page begins loading, which can reduce the time it takes for the request to be sent and the response to be received.

The preconnect browser hint can be a huge help when it comes to speeding up request time and avoiding FOIT and FOUT when loading web fonts on your website. By using this technique, you can establish a connection to the server where the font is hosted as soon as the page begins loading, which can make a big difference in how quickly the font is downloaded and displayed.

Font-face descriptors

We just saw how we could reduce the time it takes to fetch the web font, however we still haven't got around the layout shift yet.

When working with fonts, there several different measurements that are used to describe the characteristics of a font. Some of the most important measurements within a font include:

  • Ascender: The line that defines the tops of the ascenders on certain letters, such as b, d, f, h, k and l; parts of the letters that extend above the x-height.

  • Meanline: The line that defines the tops of the lowercase letters with bowl shapes, such as o and d.

  • Baseline: The line on which most letters sit, used as a reference point for positioning the letters in a line of text.

  • X-Height: The height defining the difference between the meanline and baseline. This property helps determine the size and spacing of the letters.

  • Descender: The line that defines teh bottoms of the descenders on certain letters, such as g, j, p, q, and y; parts of the letters that extend below the baseline.

Since fonts have different measurements, we can end up with layout shift even if the fallback font and the web font have the same font-size.

The pink line represents the x-height, meaning the difference between the meanline and baseline.

For this reason, we need more control over the size of the rendered font besides its font-size to reduce the layout shift. One approach is by using the size-adjust @font-face descriptor.

size-adjust

The size-adjust font-face descriptor scales all metrics associated with the font by a specified percentage. By adjusting the size to match the web font, the size of the text will remain (roughly) the same even if the font is swapped from the fallback to the web font, which can help prevent unexpected changes in the layout of the page.

Let's say you want to use the Poppins web font on your website, but you want to use the Baskerville font as a fallback in case the web font doesn't load. In this case, you can use the size-adjust property to make sure the size of the text remains consistent, no matter which font is being used. This means you could either adjust the size of the Poppins font to match the Baskerville font, or vice versa.

Since the font-size is set in the CSS for the same element, we can't use this property to change the size of the falback font, as it's not possible to differentiate the size for the fallback font or the web font. However, with the size-adjust property, we can set the size of the font in the font-faceproperty, which applies the adjusted measurements to all text using that font.

h1 {
  font-family: "Poppins", "Baskerville";
  font-size: 64px;
}

@font-face {
  font-family: "Poppins";
  src: url(https://fonts.googleapis.com/css2?family=Poppins) format("woff2");
}

@font-face {
  font-family: "Baskerville";
  src: local("Baskerville");
  size-adjust: 125%;
}

We avoid most to all layout shift, since the fallback font's measurements now closely match the web font.

While adjusting the size-adjust property can help prevent layout shift when using web fonts, it isn't always sufficient.

If you want even more control over the rendered font, you can use other CSS descriptors such as ascent-override, descent-override, and line-gap-override to define the ascent metric (height above the meanline), descent metric (height below the baseline), and line height, respectively. This can help ensure that the fallback font closely matches the web font's layout and appearance.

We've been using a serif fallback font even when the web font is sans-serif, but you can get better results by choosing a fallback font that matches the type of font you're using. For example, if you're using a sans-serif web font, you should choose a sans-serif fallback font such as Arial. This can help ensure that the font measurements are as similar as possible.


next/font module

Addingpreconnect and font-facedescriptors can improve the performance of fonts on your website by speeding up their loading time and defining their characteristics. However, manually adding these optimizations can be tedious and prone to errors. Ideally, we want our fonts to be optimized automatically.

This is where the next/font module comes in. This module was introduced in Next.js 13 and allows you to easily add local and Google fonts to your website without worrying about the details of optimization.

The next/font module makes it much easier to work with local and web fonts by automatically:

  • Downloading the web font at build time and serving it locally

  • Adding a fallback font and automatically adjusting its measurements to resemble the chosen web font as closely as possible.

You can use it by importing a font from the next/fontmodule and using the font instance to apply it to specific components in your website.

import { Poppins } from "next/font/google";

const poppins = Poppins({ weight: "600", subsets: ["latin"] });

export default function Title() {
  return <h1 className={poppins.className}>The Acme Blog</h1>;
}

Self-Hosting Fonts

At build time, the next/fontmodule downloads both the font stylesheet from fonts.googleapis.com, and the font files defined in the @font-face src's that was previously fetched.

The fonts are now available locally, without requiring any external requests.

In January 2022, the German Court ruled that using Google Fonts violates GDPR regulations. The next/font module helps improve privacy by allowing web fonts to be self-hosted.



Automatic Matching Fallback Font

The next/fontmodule not only lets you serve fonts from your own domain, but it also automatically provides a custom fallback font that closely matches the intended web font, and even calculates the necessary measurements such as size-adjust, ascent-override, descent-override, and line-gap-overrideto make the fallback font closely match the web font.

/* latin */
@font-face {
  font-family: '__Poppins_9e171c';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/_next/static/media/d869208648ca5469.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, ...;
}

@font-face {
  font-family: '__Poppins_Fallback_9e171c';
  src: local("Arial");
  ascent-override: 92.83%;
  descent-override: 30.94%;
  line-gap-override: 8.84%;
  size-adjust: 113.11%
}

The fallback font values are calculated automatically at build time


When using the next/fontmodule to display text on your website, you'll likely experience little to no layout shift when the font is changed.

Preloading

The next/fontmodule also automatically preloads fonts when a subset has been defined. This can help both performance and user experience.

A preloaded font is downloaded as soon as possible, typically before the browser starts rendering the page. This avoids the potential for the font to render incorrectly or change while the page is loading, creating a seamless experience.


Conclusion

Using web fonts can be challenging due to issues like FOUT and FOIT. However, with some optimizations like adding preconnect and using font-face descriptors like size-adjust, ascent-override, descent-override, and line-gap-override, you can improve the performance and appearance of your web fonts.

The next/font module makes working with web fonts significantly easier by automatically implementing these optimizations and serving the fonts from your own domain. This can help create a more seamless user experience and improve the overall performance of your website, resulting in better Core Web Vitals.




Conclusion

Using web fonts can be challenging due to issues like FOUT and FOIT. However, with some optimizations like adding preconnect and using font-face descriptors like size-adjust, ascent-override, descent-override, and line-gap-override, you can improve the performance and appearance of your web fonts.

The next/font module makes working with web fonts significantly easier by automatically implementing these optimizations and serving the fonts from your own domain. This can help create a more seamless user experience and improve the overall performance of your website, resulting in better Core Web Vitals.