//

Improving Next.js Performance

Next.js is a zero-config React framework for building server-side and static web applications bundled with enhanced developer experience with features for products such as smart bundling, TypeScript support, route pre-fetching, hybrid static & server rendering, and more.

If you have been using Next.js in production, you may want to explore a few different ways to improve your web application performance and get the best possible outcome from what Next.js provides you.

Table of Contents

  1. Select the right rendering mode
  2. Defer loading non-critical scripts to load when the page is idle
  3. Image Optimization with the Image component
  4. Code-splitting client-side code to reduce initial bundle size
  5. Server-side rendering with React Server components (Beta)
  6. Faster build times with SWC

Select the right rendering mode

Next.js at the moment supports three different methods of rendering pages that are built into the framework. Understanding how these methods work will greatly help you build your web application for performance.

Server-Rendering (SSR)

In response to each request, renders the app on the server, and then sends the rendered HTML and JavaScript back to the client.

Client-Side Rendering (CSR)

Renders the app in the browser on the client at run time.

Static Rendering (SR)

Usually generating a static HTML page for every URL. Pre-built app, or renders the app at build time (e.g. when you run the npm run build or yarn build command).

React Server components

React Server Components which is available in React 18 beta, will render components entirely server-side, making it possible to have a zero-KB client-side bundle.

Now let's look at the ways we can improve the performance of these methods.

Server-Rendering (SSR)

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

export async function getServerSideProps({ query, res }) {
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=43200, stale-while-revalidate=60'
  );

  const { title, body, imageUrl } = await fetch('YOUR_API_URL');

  return {
    props: {
      generatedAt: Date.new().toString(),
      title,
      body,
      imageUrl
    }
  };
}

res.setHeader - This will cache the content of this page for 12 hours. After 12 hours, the content will continue to be served for a grace period of 60 seconds as new data is fetched. Then, the CDN will store a fresh copy for the next user.

This will enable you to offer server-side rendered dynamic content much faster to the users.

Static Rendering (SR)

This is the fastest way to load your website. Statically rendered pages are generated at the time your site is built. If your website doesn’t have a lot of content, and it doesn’t change between deployments, statically rendered pages will be the best option for you.

This is the default rending method for Next.js when getServerSideProps and getInitialProps are not being used in your page components.

Incremental static regeneration (ISR)

Incremental static regeneration is great for blogs and articles. If you publish content that can be rendered once per deployment, and it won’t change that often after, SR is the best option to go for.

For instance, let's say you deploy your web application every time you create new content/blog pages, this is the ideal option. But if the publication process involves third-party CMS platforms like Sitecore and the publication process doesn't involve deploying your codebase, then SSR is the best option to dynamically load content.

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

  • User requests the page (e.g. /post).
  • If this is the first request for the /post page since the last deployment, the server will generate and store the page in cache until the next deployment.
  • Each following request to /post will then be served the pre-generated page from a cache.

Following this approach in a content-heavy web application (with many hundreds or even thousands of pages or articles), you will be able to reduce build wait time significantly while still offering viewers lightening fast page renders.

To summarise the rendering methods:

  • For dynamic content or when the query parameters alter the rendered content of your page, use Server-Side Rendering (SSR).
  • For scenarios where you have plenty of pages but don’t want to wait for them to be statically generated, use Incremental Static Regeneration (ISR).
  • If any of the above use cases don’t apply, use Statically Rendered pages.

Read more about different Next.js rendering methods here: https://szaranger.medium.com/nextjs-client-side-server-side-static-rendering-1836337998b9

Defer loading non-critical scripts to load when the page is idle

Websites often use third-party scripts for analytics, ads, customer support widgets, and consent management. However, this can introduce problems that impact both user and developer experience:

  • Some third-party scripts are heavy on loading performance and can drag down the user experience, especially if they are render-blocking and delay any page content from loading
  • Developers often struggle to decide where to place third-party scripts in an application to ensure optimal loading

The Script component makes it easier for developers to place a third-party script anywhere in their application while taking care of optimizing its loading strategy.

With next/script, you decide when to load your third-party script by using the strategy property:

<Script src="https://connect.facebook.net/en_US/sdk.js" strategy="lazyOnload" />

There are three different loading strategies that can be used:

  • beforeInteractive: Load before the page is interactive
  • afterInteractive: (default): Load immediately after the page becomes interactive
  • lazyOnload: Load during idle time

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

Image Optimization with the Image component

Next’s Image is a powerful component that simplifies the responsive image generation, optimized using compression algorithms.

Here are some of the benefits of the Image component:

  • Built-in WebP and AVIF support. The browser can display AVIF, WebP, PNG, SVG, or any other image-based MIME type by making the servers to be able to decipher which format to send via HTTP header. Eg: Accept: image/avif, image/webp, image/apng, image/svg+xml, image/,/*;q=0.8.
  • Automatically avoid Cumulative Layout Shifts by placing a blurred or transparent placeholder image until the image has loaded.
  • Responsive images are resized on the fly, able to be cached by CDNs.
  • 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 WebP and AVIF by adding them to the to next.config.js:

{
  images: {
    formats: ['image/webp', 'image/avif']
  }
}

By offering AVIF images to supported clients, you can reduce large detailed blog illustrations by 60% for large desktop displays.

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

Read more about the options here: https://nextjs.org/docs/api-reference/next/image

Code-splitting client-side code to reduce initial bundle size

Code-splitting using dynamic imports, we can keep the Next.js bundle sizes small.

Code-splitting allows you to lazily-load dependencies or application code, as required by the user. This is done by splitting JavaScript bundles into smaller chunks which are small JavaScript files.

This helps us to accomplish the following:

  • Faster initial page loads
  • Only load what’s required, so fewer to download
  • Fewer JavaScript to parse, compile and execute helping slower mobile devices
  • Chunks can be cached individually

In the example below, we are heavily reducing initial JavaScript bundle size by dynamically loading the Ad component, keeping them from appearing in the main JavaScript bundle:

// Before
import Ad from 'components/Ad';

// After
import dynamic from 'next/dynamic';

const Ad = dynamic(() => import('components/Ad'));

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

Server-side rendering with React Server components (Beta)

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

// pages/[post].server.js
import React, { Suspense } from 'react';
import dynamic from 'next/dynamic';

import Layout from 'components/Layout.server.js';
import Content from 'components/Content.server.js';
import { getContent } from 'data';

const Ad = dynamic(() => import('components/Ad.client.js'));

export default function Post({ slug }) {
  const { title, createdDate, content } = getContent({ slug: slug });

  return (
    <Layout>
        <article>
          <header>
            <h1>{title}</h1>
            <p>Published: {createdDate}</p>
          </header>
    
          <Suspense>
            <Ad />
          </Suspense>
    
          <Content source={content} />
        </article>
    <Layout>
  );
}

In the above example, we fetch content from a static data folder or a CMS and render markdown. The resulting client-side JavaScript from this page will only include the Ad, which is dynamically loaded. This way, you can first deliver server-rendered content, then enhance the page with client-side components, and then React will resolve Suspense boundaries client-side.

Learn more about Server Side Rendering with Suspense in React 18

Faster build times with SWC

Next.js uses webpack and Babel under the hood to compile JavaScript bundles. Babel using a JavaScript runtime and has been slow to compile.

$ next build

With the arrrival of SWC, a new Rust based compiler for JavaScript, which has superior compilation speeds, Next.js 12 announced SWC as it's new experimental JS bundler. This means that next build, live-reloads and deployments will all be faster.

In order to kick-in SWC as the compiler, make sure to remove the babel.config.js file.

Summary

To improve your web application performance and get the best possible outcome from what Next.js provides you, we can:

  1. Select the right rendering mode
  2. Defer loading non-critical scripts to load when the page is idle
  3. Image Optimization with the Image component
  4. Code-splitting client-side code to reduce initial bundle size
  5. Server-side rendering with React Server components (Beta)
  6. Faster build times with SWC