First impressions on Next.js Automatic Font Optimization
Tags: nextjs, performance
Published: 2021-05-01 08:49:17

<figcaption>Cover photo: Lobster by <a href="https://unsplash.com/photos/OJPBfUqgsRU">Meritt Thomas</a></figcaption>

---

**Automatic Webfont Optimization** is a new feature that shipped with [Next.js 10.2](https://nextjs.org/blog/next-10-2#automatic-webfont-optimization). I tried it fresh off the grill, and here is what I think.

---

### Misnomer (for now)

Despite the name, currently it only works for Google Fonts loaded from `fonts.googleapis.com`. It does not work with other sources, including eg. mirrored Google fonts in [regions that block Google domains](https://www.labsterx.com/blog/things-to-consider-when-building-a-website-in-china-9-shtml/).

As per [their blog post](https://nextjs.org/blog/next-10-2#automatic-webfont-optimization), they will add integration with other font providers in the future.

### How to use it?

Add a `link` tag that loads the Google fonts CSS to Next.js’ built-in `Head` component.

```jsx
import Head from 'next/head';

export default function MyPage() {
  return (
    <div>
      <Head>
        <link
          href="https://fonts.googleapis.com/css2?family=Lobster"
          rel="stylesheet"
        />
      </Head>
      <p>Hello world!</p>
    </div>
  )
}
```

`Head` is a special component that enables us to append the HTML `<head>` element from our page components or from the custom `Document` component.

More info: [next/head](https://nextjs.org/docs/api-reference/next/head) | [next/document](https://nextjs.org/docs/advanced-features/custom-document)


### How does it work?

Our code above is compiled into this on build:

```html
<head>
  <!-- ... other head/meta tags -->
  <link rel="stylesheet" data-href="https://fonts.googleapis.com/css2?family=Lobster">
  <!-- ... -->
  <style data-href="https://fonts.googleapis.com/css2?family=Lobster">
    @font-face{font-family:'Lobster';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/lobster/v23/neILzCirqoswsqX9zoKmM4MwWJU.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}
  </style>
</head>
```

Without optimization:
1. the `link` tag loads the Google fonts CSS, `https://fonts.googleapis.com/css2?family=Lobster`
2. the CSS `@font-face` loads the actual font source, `https://fonts.gstatic.com/s/lobster/v23/neILzCirqoswsqX9zoKmM4MwWJU.woff2`

With optimization, we skip step (1). Notice that the `href` attribute is changed into `data-href`. Instead, the style declarations are inlined and injected to the `<head>`. The browser requests the font file(s) in step (2) directly.


### Why use it?

If you use Lighthouse, you may have come across the recommendation to [eliminate render-blocking resources](https://web.dev/render-blocking-resources/).

When we use the original `<link>` tag, the browser _pauses_ to request the URL in the `href` attribute, eg. `https://fonts.googleapis.com/css2?family=Lobster`. When it succeeds or fails, it continues rendering the rest of the page.

The font files declared in the `@font-face` CSS itself, eg. `https://fonts.gstatic.com/s/lobster/v23/neILzCirqoswsqX9zoKmM4MwWJU.woff2`, are loaded asynchronously. They _don't block_ the rendering process.

By using inlined style, we eliminate this render-blocking instance, thus improving page performance.


### Best paired with...

Add these for optimal results.

#### `preconnect` to the font host origin

```jsx
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
```

We tell the browser that we _intend to connect_ to `fonts.gstatic.com` and retrieve our font file (`https://fonts.gstatic.com/s/lobster/v23/neILzCirqoswsqX9zoKmM4MwWJU.woff2`) from there.

#### Add display=swap to prevent FOUT

```jsx
<link
  href="https://fonts.googleapis.com/css2?family=Lobster&display=swap"
  rel="stylesheet"
/>
```

I mentioned above that font files load asynchronously. It does not block rendering (yay), but it could mean a _"flash of invisible text"_ (oh no) when the text content is rendered before the font file finishes loading. Adding `display=swap` renders the text with the available fallback font, then _swaps_ it with the intended web font once it finishes loading.


### Caveats

🔮 _Future readers: This post is written three days after the public launch. Things described here may have changed when you read this._

Be aware of these possible issues if using this in production.

#### Issue 1: Does not fire on client-side navigation change

When using client-side navigation, such as when using Next.js `Link` component, the `style` tag is _not_ injected into the `<head>` in the destination route. This causes issue if we use different fonts in each page route component.

For example, we have [Page A](https://example-nextjs-webfont-optimization.vercel.app) that uses the _Permanent Marker_ handwriting typeface and [Page B](https://example-nextjs-webfont-optimization.vercel.app/hello-lobster) that uses the legendary _Lobster_ typeface. Accessing either page directly works fine. Click on those pages and you'll see the respective typefaces.

But try navigating to Page A from [Page B](https://example-nextjs-webfont-optimization.vercel.app/hello-lobster) by clicking the _"go back"_ link. Page A is rendered with the default typeface instead of _Permanent Marker_. If you open DevTools, you'll see that Page A's font stylesheet is _not_ injected into the `head`, so the font file is not loaded.

**Solution:** Load all fonts in the [custom Document](https://nextjs.org/docs/advanced-features/custom-document) `Head` instead of in page routes.

**Examples**

- ❌ DON’T: Load different fonts in page components 
  - code: https://github.com/ekafyi/example-nextjs-webfont-optimization/blob/main/pages/index.tsx#L16-L19
  - preview: https://example-nextjs-webfont-optimization.vercel.app/hello-lobster
- 👍🏽 DO: Load fonts in custom _document
  - code: https://github.com/ekafyi/example-nextjs-webfont-optimization/blob/with-custom-document/pages/_document.tsx#L26-L29
  - preview: https://example-nextjs-webfont-optimization-e0t88ypsc-ekafyi.vercel.app/hello-lobster

#### Issue 2: Stops working arbitrarily

The first time I used this feature, it _did_ work and rendered this code.

```html
<link rel="stylesheet" data-href="https://fonts.googleapis.com/css2?family=Lobster">
<!-- ... -->
<style data-href="https://fonts.googleapis.com/css2?family=Lobster">
  @font-face{font-family:'Lobster'; ... }
</style>
```

However, in subsequent builds it renders this instead.

```html
<link rel="stylesheet" data-href="https://fonts.googleapis.com/css2?family=Lobster">
<!-- ... -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lobster">
```

Instead of inlining the style, now it only duplicates the render-blocking stylesheet link tag. In addition to checking the `head` tag from the DevTools, running a [Lighthouse test](https://lighthouse-dot-webdotdevsite.appspot.com//lh/html?url=https%3A%2F%2Fexample-nextjs-webfont-optimization-1fe1cxpz6-ekafyi.vercel.app%2F) gives me an _"Eliminate render-blocking resources"_ warning, which confirms that font optimization has indeed stopped working.

I managed to reproduce this in a fresh repo from one of the official examples, but was not able to pinpoint the cause. The commits/builds that triggered and fixed the issue were completely unrelated, eg. adding a new page and removing a favicon. Reverting the offending changes did not affect the font optimization issue.

- commit [4656896](https://github.com/ekafyi/example-nextjs-webfont-optimization/commit/46568966da0d6d99aa9a275c2205be662b679092) — font optimization works
   - preview: https://example-nextjs-webfont-optimization-2yi6vsuvr-ekafyi.vercel.app/
- commit [d8d9735](https://github.com/ekafyi/example-nextjs-webfont-optimization/commit/d8d9735710e05fdde5ff7f13b994bff841354e9c) — font optimization does not work
   - preview: https://example-nextjs-webfont-optimization-1fe1cxpz6-ekafyi.vercel.app/
- commit [589b900](https://github.com/ekafyi/example-nextjs-webfont-optimization/commit/589b900f7e19af9d4f1e41c37ec3f68bb38f56aa) — font optimization still does not work
   - preview: https://example-nextjs-webfont-optimization-50yetu6pr-ekafyi.vercel.app/
- commit [946b980](https://github.com/ekafyi/example-nextjs-webfont-optimization/commit/946b9806cdc3664006fabb3f2c2c8d03fbc96ecd) - font optimization works again
   - preview: https://example-nextjs-webfont-optimization-n2kn8qwwb-ekafyi.vercel.app/

**Solution:** I did not find a solution and this was a deal breaker to me, so I opted out of automatic font optimization by adding this option in `next.config.js` and roll my own font optimization.

```js
module.exports = { 
  // ...
  optimizeFonts: false 
}
```


### Conclusion

I like the premise of this feature—improve performance by optimizing Google fonts out of the box—but _to me_ it is still too unstable to adopt for now.

Also worth noting that it's fairly simple to implement web font optimization ourself using these techniques.

- https://csswizardry.com/2020/05/the-fastest-google-fonts
- https://web.dev/optimize-webfont-loading/
- https://ahmadawais.com/google-fonts-load-faster/
- https://dev.to/masakudamatsu/loading-google-fonts-and-any-other-web-fonts-as-fast-as-possible-in-early-2021-4f5o


### What’s (ahem) _next_?

Discussions on future plans:
- [RFC] Automatic Font Fallback for layoutshiftless font-display: swap https://github.com/vercel/next.js/discussions/24438
- Add preconnect to Automatic Webfont Optimization https://github.com/vercel/next.js/discussions/24645