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>
)
}
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>
Without optimization:
- the
link
tag loads the Google fonts CSS,https://fonts.googleapis.com/css2?family=Lobster
- 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" />
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"
/>
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
- ❌ DON’T: Load different fonts in page components
- 👍🏽 DO: Load fonts in custom _document
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>
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">
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.
- commit 4656896 — font optimization works
- commit d8d9735 — font optimization does not work
- commit 589b900 — font optimization still does not work
- commit 946b980 - font optimization works again
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
}
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
Top comments (6)
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!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.
It looks like a fix dropped somewhere last week (merged around Next 10.2.1 to 10.2.3) 🎉
ah, good to hear!
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.Thanks This works great for Google fonts but not for local fonts!
How to deal with custom fonts? Anybody font a solution?