DEV Community

Cover image for First impressions on Next.js Automatic Font Optimization
Eka
Eka

Posted on • Updated on

First impressions on Next.js Automatic Font Optimization

Cover photo: Lobster by Meritt Thomas

Automatic Webfont Optimization is a new feature that shipped with Next.js 10.2. 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.

As per their blog post, 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.

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 | next/document

How does it work?

Our code above is compiled into this on build:

<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>
Enter fullscreen mode Exit fullscreen mode

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.

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

<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
Enter fullscreen mode Exit fullscreen mode

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

<link
  href="https://fonts.googleapis.com/css2?family=Lobster&display=swap"
  rel="stylesheet"
/>
Enter fullscreen mode Exit fullscreen mode

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 that uses the Permanent Marker handwriting typeface and Page B 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 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 Head instead of in page routes.

Examples

Issue 2: Stops working arbitrarily

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

<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>
Enter fullscreen mode Exit fullscreen mode

However, in subsequent builds it renders this instead.

<link rel="stylesheet" data-href="https://fonts.googleapis.com/css2?family=Lobster">
<!-- ... -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lobster">
Enter fullscreen mode Exit fullscreen mode

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 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.

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.

module.exports = { 
  // ...
  optimizeFonts: false 
}
Enter fullscreen mode Exit fullscreen mode

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.

What’s (ahem) next?

Discussions on future plans:

Top comments (6)

Collapse
 
wns3645 profile image
Junhee Park • Edited

Thanks for great post.
BTW, link tag for preconnect is aoutmatically injected by next.js when font optimziation is enabled.(github.com/vercel/next.js/pull/25346) So we don't need to add <link rel="preconnect" .../> for google font anymore!

Collapse
 
doctorderek profile image
Dr. Derek Austin 🥳

Great post, thanks for sharing & also for the follow up. I also re-ran some Lighthouse testing and found that the feature no longer seems to be working.

Collapse
 
doctorderek profile image
Dr. Derek Austin 🥳

It looks like a fix dropped somewhere last week (merged around Next 10.2.1 to 10.2.3) 🎉

Collapse
 
ekafyi profile image
Eka

ah, good to hear!

Collapse
 
engelmav profile image
Vincent Engelmann

I'm on version 11. Just got the Eliminate render-blocking resources warning again for fonts that were successfully inlining before. Maybe a bug snuck back in.

Collapse
 
shrroy profile image
Shrroy

Thanks This works great for Google fonts but not for local fonts!
How to deal with custom fonts? Anybody font a solution?