Next.js Performance: Making a Fast Framework Even Faster

Ben Schwarz

Ben Schwarz

December 2, 2021

Illustrated by

 Jeffrey Phillips

Next.js framework is known for quick navigations and excellent developer experience. The creators and maintainers of Next.js invest time in tooling and options that help make it fast.

Having an optimised framework is a great starting point, but as your sites and apps grow, you’ll need to make the most of your frameworks’ features to offer your users a superior level of experience.

In this post, we’ll describe how your team can get the most out of Next.js when it comes to performance.

Dynamically load client-side code to reduce first load JavaScript

One of the most effective ways to keep Next.js client-side bundles small is to utilise dynamic imports, a technique also known as code-splitting.

Code-splitting allows you to lazily-load application code or dependencies, as required by each user journey. In practice, this means that JavaScript bundles are broken into smaller chunks (many small JS files).

This benefits users in numerous ways:

  • Less to download (only load what’s required)
  • Less JavaScript to parse, compile and execute (a big deal for slower devices!)
  • Faster initial page loads
  • Chunks can be individually cached

In the following example, we significantly reduced first-load JavaScript by dynamically loading SVG logo assets, keeping them from appearing in the main JavaScript bundle:

1// components/CompanyLogos/index.js
2
3// Before
4import TastyLogo from 'images/logos/logo-tasty.svg'
5
6// After
7import dynamic from 'next/dynamic'
8
9const TastyLogo = dynamic(() => import('images/logos/logo-tasty.svg'))

After switching SVG logos to be loaded dynamically, we saw a bundle reduction of 82KB. Now each SVG asset is inlined to the page without appearing in JavaScript bundles:

calibreapp.com JavaScript bundle visualisation
The highlighted section of the treemap visualisation is no longer included after using dynamic imports.

For a complete list of options for dynamic loading, see the official Next.js documentation.

Defer loading non-essential scripts until the page is idle

The reality for many developers is there’s often no choice but to add third party scripts for analytics, tracking or customer communication tools, simply because there’s a business requirement for it.

Adding heavy scripts to a page is a sure-fire way to slow down user experiences, but that doesn’t mean we don’t have control over how these resources load! By using next/script, you can specify when the browser should fetch selected scripts:

1// pages/index.js
2import Script from 'next/script'
3
4export default function Home() {
5 return (
6 <>
7 // page components go here
8 <Script
9 src="https://www.google-analytics.com/analytics.js"
10 strategy="lazyOnload"
11 />
12 </>
13 )
14}

In the above example, Google Analytics will be fetched after all other page resources once the browser has become idle. It’s crucial to load critical page resources first without making visitors wait for third-party add-ons and tracking scripts.

next/script comes with three loading strategies:

  • lazyOnload - Load when the page is idle
  • beforeInteractive - Load before the page is interactive
  • afterInteractive - Load immediate after the page is interactive (default)

If you’d like a script to have minimal impact on your page, use lazyOnload.

Find out more about next/script in the official Next.js documentation.

Free tool
Are you using a chat widget or a support tool? Install our free library, React Live Chat Loader. It improved our Time to Interactive by 30%!

Optimise images with the Image component

Next’s <Image /> is a powerful component that trivialises the generation of responsive images, optimised using the latest compression algorithms.

Here are some of the highlights of <Image />:

  • Responsive images are resized on the fly, able to be cached by CDNs.
  • AVIF and WebP support is built in. Servers are able to decipher which format to send via HTTP header. E.g: Accept: image/avif, image/webp, image/apng, image/svg+xml, image/*,*/*;q=0.8 signifies that the browser can display AVIF, WebP, PNG, SVG, or any other image based MIME type.
  • Cumulative Layout Shifts are automatically avoided by placing a transparent or blurred placeholder image until the image has loaded.
  • It’s adaptable to responsive design. The layout attribute allows you to specify images that should scale to fit or fill their containers.

You can enable AVIF and WebP by adding configuration to next.config.js:

1// ./next.config.js
2{
3 images: {
4 formats: ['image/avif', 'image/webp']
5 }
6}

By offering AVIF images to supported clients, we reduced large detailed blog illustrations from 300Kb to around 100Kb for large desktop displays.

Tip
AVIF are more CPU intensive to generate. In development mode, AVIF generation may slow down your workflow and spin up laptop fans.

Next.js’ Image component provides the power and flexibility that most pages require and is a solid upgrade on base functionality provided by native <img /> elements.

We recommend you familiarise yourself with its many configurable options so that you can provide users with fast-loading images every time.

Choose the right rendering mode

Next.js currently comes with three methods of rending pages. It’s valuable to familiarise yourself with the options available and their inherent strengths and weaknesses.

While these rendering methods are built directly into Next.js, their concepts can be applied to most tech stacks, though the level of work required to achieve them will vary greatly.

Next.js offers a path of low resistance to get good results, and it’s part of why we love the framework. Below, we’ve described each render mode and our recommendations to get the best bang for the buck.

Server Side Rendering (SSR)

Server rendered pages are generated for each request that comes into your server. You’ll use SSR for highly dynamic content that needs to stay up to date.

If the content is considered “fresh” for a given time, you can add Cache-Control headers to instruct Vercel/CDNs to cache the resulting HTML for a given period:

1export async function getServerSideProps({ query, res }) {
2 // Cache the content of this page for 12 hrs
3 // After 12 hrs, the content will continue to be served
4 // for a grace period of 60 seconds as new data is fetched
5 // Then, the CDN will store a fresh copy for the next user :-)
6 res.setHeader(
7 'Cache-Control',
8 'public, s-maxage=43200, stale-while-revalidate=60'
9 )
10
11 const { title, body, imageUrl } = await getPageDataFromCMS('home')
12
13 return {
14 props: {
15 generatedAt: Date.new().toString(),
16 title,
17 body,
18 imageUrl
19 }
20 }
21}

In the example above, the page will fetch the dynamic data from a CMS when a user requests it.

If you’re using Vercel or an intermediate CDN/reverse proxy server, the resulting HTML response will be cached for 43200 seconds (12 hours). When the page cache expires, the CDN will continue to serve it for another 60 seconds while it is re-generated by the server.

By partnering SSR pages with well-thought-out caching strategies, you’ll be able to offer fast dynamic content to all viewers.

Incremental Static Regeneration (ISR)

Incremental static regeneration is excellent for blogs and publications. If you’ve got content that can be rendered once (per deployment), and it won’t change after that, ISR might be your new best friend.

Much of the content we read online is written to attract two distinct audiences: people and SEO spiders. In both cases, content must arrive as quickly as possible.

If you statically build pages during deployment, you’ll spend time waiting for the pages to build.

An ISR page differs by waiting until the browser requests the page before generating it. That process looks like this:

  • A person requests the page (e.g. /home).
  • If no one has requested /home since the last deployment, the server will generate and store the page in cache until the next deployment.
  • Each person who then requests /home will be served the pre-generated page from a cache.

By using this approach in a content-heavy blog (with many hundreds or even thousands of articles), you’re able to circumvent build wait time significantly while still offering viewers sub-second page renders.

Statically rendered

Statically rendered pages are generated when your site is built. If you don’t have a lot of content, and it doesn’t change between deployments, statically rendered pages will always offer the overall fastest load (without concerning yourself with headers or page caching).

Statically rendered pages are the default Next.js rending method when getServerSideProps and getInitialProps aren’t in use.

To recap rendering modes options:

  • Use Server Side Rendering (SSR) for dynamic content or when query-params alter the rendered content of the page.
  • Use Incremental Static Regeneration (ISR) for scenarios when you have a lot of pages but don’t want to wait for them to be statically generated.
  • Use Statically Rendered pages any time that the previous use cases don’t apply.

Feature preview: React Server Components

In December 2020, the React team announced React Server Components, which are currently available as a React 18 Next.js beta feature. While Next.js already offers SSR, ISR and Static rendering, a JavaScript bundle is still required in all cases, even when content is not dynamic.


React Server Components will render components entirely server-side, making it possible to have a zero-KB client-side bundle.


With Server Components, you’re able to opt-in which parts of your application are rendered on the server and when client-side code is required. In Next.js, a server component will be denoted by filename, e.g., page-name.server.js, whereas a client-side component will use page-name.client.js.

In the following example, we can fetch content from a CMS, import a date-time utility (date-fn) and render markdown. The resulting client-side JavaScript from this page will only include the ShareMenu, which is dynamically loaded. React will resolve <Suspense> boundaries client-side. That way, you can first deliver server-rendered content, then enhance the page with client-side components:

1// pages/[post].server.js
2import dynamic from 'next/dynamic'
3import React, { Suspense } from 'react'
4import format from 'date-fns/format'
5
6import Markdown from 'components/Markdown.server.js'
7
8const ShareMenu = dynamic(() => import('components/ShareMenu.client.js'))
9
10import { getPostContent } from 'data/cms'
11
12export default function Post({ slug }) {
13 const post = getPostContent({ slug: slug })
14
15 return (
16 <article>
17 <header>
18 <h1>{post.title}</h1>
19 <p>Published: {format(post.publishedAt, "h:mma 'on' MMM do yyyy")}</p>
20 </header>
21
22 <Suspense>
23 <ShareMenu />
24 </Suspense>
25
26 <Markdown source={post.body} />
27 </article>
28 )
29}
Tip
We’re excited to see how React Server Components will help us optimise our pages for speed, but bear notice: they’re not yet ready for production usage. Progress is steady, so stay tuned!

Feature Preview: near instant bundling

All JavaScript based frameworks require some form of JavaScript compilation. Under the hood, Next.js uses Webpack and Babel to compile JavaScript bundles. Babel has served us well up until now, but its speed is limited by using a JavaScript runtime.

Enter SWC, a new Rust-lang based compiler for JavaScript. Next.js 12 announced SWC as a new experimental JS bundler. SWC offers far superior compilation speeds, meaning that live-reloads, next build and deployments will all be faster.

Tip
In the case of calibreapp.com, next build went from 1 minute 17 seconds (Babel), to 22 seconds (SWC).

While SWC actively offers website viewers a superior experience, developers can work more quickly and move faster. Let’s use that gained time to enhance user experiences as best we can!


By learning how to use and configure Next.js effectively, you’ll be able to offer customers better, faster web experiences with less effort. We’ll be sure to keep this article up to date with Next’s best practices. If you have other recommendations, let us know!

Ben Schwarz

Ben Schwarz

Ben is the founder and CEO of Calibre. He uses his experience in far-reaching Open Source projects and web standards to build tools for a better, more accessible web. Find him on Twitter or LinkedIn.

Become a site speed expert

Join thousands of subscribers keeping up-to-date with web performance news and advice.

The best performance newsletter I’ve come across. Highly recommended.

Addy Osmani

Addy Osmani

Engineering Manager at Google Chrome