FOR DEVELOPERS

Optimizing Build Performance in Next.js

Optimizing Build Performance in Next.js

Next.js is a React.js framework that offers powerful capabilities that greatly impact application performance. This is achieved through its build process. But to make the most of it, we need to understand how it handles builds and fine-tunes this process to give an optimized and performant application. With this knowledge of of enhancing Next.js build performance we’ll have better user experience, outstanding application performance, quick loading times, SEO advantages, and more.

In this article, we’ll look at several optimizations that can enhance build efficiency in the Next.js ecosystem. We will explore every aspect, from its inbuilt optimization features to caching and serverless functions.

Furthermore, we will construct two sample projects to explain and illustrate the effects of these optimizations. One sample showcases the full potential of Next.js optimization.The other will be intentionally unoptimized to show the stark differences between the two approaches. By comparing the results, we can appreciate the profound implications of optimizing the build of a Next.js application.

Using Next.js built-in optimization features

Next.js provides us with some built-in optimization features that enhance the performance and user experience of web applications. These features include the following:

Image optimization

A common source of performance bottlenecks in web applications is images. For this reason, Next.js provides us with the Image component—a solution that is nothing short of a game-changer. It comes with some automatic optimizations that promise to revolutionize image handling.

  • Size optimization: Next.js is excellent at optimizing the serving of local images. This optimization ensures that the images are served in the appropriate sizes, reducing both load times and bandwidth usage. No need to manually resize images for various screen resolutions and sizes. To serve optimized remote files, we need to specify the size and supported URL patterns in the "next.config.js" file.
  • Visual stability: The "Image" component ensures that there are no annoying layout shifts, maintaining a consistent user experience. Images without dimensions can cause layout shifts. Next.js ensures that the height and width of an image is required by default.
    With this requirement and its ability to maintain "aspect-ratio", Nextjs provides visual stability and prevents layout shift.
  • Faster page loading: The "Image" component helps speed up page load by serving images and contributing to faster loading times. By caching, preloading, lazy loading, and optimizing, it reduces waiting times for users, even on slow connections and with blur capability.
  • Asset flexibility: Next.js can provide responsive image resizing by adjusting to any screen size and device orientation.

Example:

<Image  src="https://res.cloudinary.com/theodorekelz/image/upload/v1698318921/zyv1owydclvkml97ecrr.jpg"
  alt="Picture of the author"
  width={500}
  height={500}
  priority
/>

// next.config.js module.exports = { images: { remotePatterns: [ { protocol: "https", hostname: "res.cloudinary.com", port: "", pathname: "/theodorekelz/**", }, ], }, };

The above code snippets were utilized to display an image sourced from a CDN. The dimensions of the image are 500 pixels in both width and height.

Next.js will be able to use the picture as the largest contentful paint (LCP) thanks to the "priority" property. And to prevent malicious usage and safely enable image optimization, we configured the "next.config.js" file.

Font optimization

We want to optimize fonts if we are to achieve privacy, performance, and a seamless user experience. The next/font component of Next.js optimizes fonts and improves privacy and performance by preventing external network requests for fonts.

A notable characteristic is its ability to smoothly incorporate any Google font into our application. Thus, the browser does not send any requests to Google. We can also use as many fonts as we want in our application, including local fonts and Tailwind CSS.

The next/link component can prefetch a page in the background, improving our app's navigation performance. If found in the viewport, the component will preload the page.

It’s a better solution than the traditional anchor tag "<a></a>" because the next/link by default, prefetches pages in the background, enhances an app’s navigation performance. This is unlike the "<a></a>" which doesn’t provide built-in prefetching. Additionally, we can pass props to the next/link to give us better control over the navigation behavior.

Example:

<Link href="/dashboard" scroll={false} preload={true}>
  Dashboard
</Link>

In the code, we used the Link component to link to the dashboard page. We have tailored it such that the page doesn't instantly scroll to the top upon visitation. We set the preload to true to enable the dashboard page to load in the background before hitting it.

Script optimization

Traditionally, using an external library with the HTML "script" tag does not result in optimal performance. The "next/script" component enhances the loading pace of web applications. It allows us to choose where to load third-party scripts based on our preferred strategy.

Example:

<Script
  src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"
  strategy="afterInteractive"
  onError={(e: Error) => {
    console.error("Jquery failed to load", e);
  }}
  onLoad={(e: Error) => {
    console.log("Jquery loaded!", e);
  }}
/>

In the code above, the "Script" tag is to load jQuery as soon as possible using the "afterInteractive" strategy. We also handled a successful load or error of a script using both the "onError" and "onLoad" properties.

Using the metadata API

Using "Metadata" helps search engines understand our content and allows users to discover our website through search results. The right keywords in the titles can increase our content’s ranking in search engines.

Metadata can be config-based by exporting a static metadata object or using the "generateMetadata" function. It could also be file-based, such as "robots.txt" or "sitemap.xml".

Using static assets

By default, Next.js caches files or static assets in the "/public" folder. The rationale is that these assets remain unchanging between visits to our website. The system caches assets for up to a year, though the duration can depend on the configuration.

Caching these static assets increases the performance of our website. Devices can retrieve these assets from their cache instead of repeatedly downloading them from the server, which not only speeds up the loading process but also enhances the user experience.

Additionally, caching static assets reduces server load, improving efficiency and saving on hosting and data transfer expenses.

Lazy loading: Dynamic imports and React.Lazy()

Lazy loading is the deferral of client components or imported libraries until needed. It improves how fast an app loads by reducing the amount of Javascript needed to show a page or route. In Next.js, we can implement this through dynamic imports and the React.js "lazy" function and "Suspense" component.

Example:

// Lazy loading using dynamic imports
import { useState } from "react";
import dynamic from "next/dynamic";

const Modal = dynamic(() => import("../components/header"));

export default function Home() { const [showModal, setShowModal] = useState(false); return ( <div> {showModal && <Modal />} <button onClick={() => { setShowModal(!showModal); }} > Display Modal </button> </div> ); }

In the code above, we use Next.js dynamic imports by deferring the loading of the "Modal" component until it is required or when the condition is satisfied.

Example:

// Lazy loading using dynamic imports React.lazy
import { lazy, Suspense } from "react";

const CardPreview = lazy(() => import("./CardPreview.js")); import Loading from "./Loading.js";

<Suspense fallback={<Loading />}> <h2>Preview</h2> <CardPreview /> </Suspense>

In the code above, we have been able to lazy load the "CardPreview" component by using the "Suspense" component and "lazy" function.

Using Next.js server-side rendering

Server-side rendering occurs when the server renders the UI instead of the client. Server rendering that is provided by Next.js can do the following:

  • Caching: Results from requests can be cached to reduce the amount of rendering and requests.
  • Data fetching: Data fetching can be moved to the server side, closer to the data source.
  • Bundle size: With server rendering, the browser doesn’t need to process large JavaScript files. The JavaScript files sent to the browser are smaller in size. This is because the server can optimize and pre-render much of the content, reducing the need for the client's browser to process large JavaScript files.
  • Initial page load and first contentful paint (FCP): Server rendering generates HTML to users immediately, without waiting for the browser to download or execute JavaScript.

All these stated above, and many more, help to improve Next.js performance.

Static site generation

If a website page doesn’t show frequently updated data and the page content does not change on every request, then we can use static site generation. Examples include marketing pages, blog posts and portfolios, e-commerce product listings, and help and documentation. Next.js can build this page once, and then you can cache it using a CDN.

Specific imports

Specific imports require that we import only necessary functions or objects instead of full packages, which reduces bundle size. When we import just the dependencies our code requires, we eliminate unnecessary bloat and ensure that the Next.js application runs smoothly and efficiently. This has a positive effect on performance, makes the code easier to maintain, and reduces the risk of bugs.

Example:

// old
import underscore from "underscore"
// new
import { map } from "underscore"

In the code above, we only needed the "map" method of the "underscore" package.

Caching

Using caching strategies like content delivery networks (CDNs) can enhance the delivery of web content. Next.js CDNs help distribute a website’s assets like images, CSS stylesheets, and JavaScript across geographically distributed servers. By leveraging CDNs, a Next.js application can achieve optimal global performance.

Another approach is browser caching—a technique that instructs a browser to locally store certain assets for a specified duration so that a returning user’s browser can retrieve static files from its local cache instead of redownloading them from the server.

Lastly, we can use service workers, which are JavaScript files that work in the background of our application. They can help cache content even offline, with or without network connectivity.

Although caching improves web content delivery, it’s important to control cache sizes to prevent them from occupying too much storage space.

Serverless functions

Using serverless functions can significantly improve performance by moving time-consuming operations to serverless functions or separate services such as Vercel Serverless Functions, Netlify Functions, AWS Lambda, and Azure Functions. Delegating resource-intensive tasks to external services can reduce build times.

Unused packages or imports

Imports and unused packages can affect build optimization and should be removed from our projects. Several tools exist that can this, including depcheck, ESLint, npm-check, manual code review, and IDE plugins.

Visualizing tools

The ability to visualize and comprehend the inner workings of a project is indispensable. Visualizing tools allow us to visualize our project’s bundle size and the build performance of our application.

Several tools are at our disposal to determine an application's performance. These tools include Webpack Bundle Analyzer, Google’s Lighthouse, Package Phobia, and Import cost VSCode extension.

Monitoring and profiling

Monitoring and profiling help identify bottlenecks, diagnose issues, and ensure that our application is running efficiently. These processes include:

  • Browser developer tools
  • Server logs and metrics
  • Error and exception monitoring
  • Third-party service monitoring
  • Regular auditing

Continuous integration and development

Setting up continuous integration and development checks can ensure optimized build performance. These checks carried out in the development process help discover early issues.

Continuous integration and development can include setting up CI/CD pipelines to automate build and deployment processes, adding performance checks in CI/CD workflows, regression testing, deployment to a staging or test environment, and documenting and reporting. All these checks increase Next.js performance on the web.

Minify and compress assets

For the best build performance, images, JavaScript, and CSS must be compressed and minified. Excellent tools for this task include Terser, UglifyCSS, ImageMagick, and ImageOptim.

These technologies decrease the size of photos without compromising their quality and simplify CSS files to just a few lines. Additionally, they perform the same functions for our JavaScript programs, which will increase JS performance and reduce bundle size.

As we will see in the article's demo section, write the code ourselves instead of relying on installed packages whenever possible.

Using latest version

Staying up-to-date with the latest advancements, updates, and improvements is crucial. Because the Next.js team continually works on optimizing the framework’s performance, it is advisable to download the current version.

Performance enhancements promise greater speed, responsiveness, and efficiency for our applications. Because software development is an iterative process, bugs and fixes are inherent during this process. Upgrading to the latest version benefits us with bug fixes, resulting in more stability and reliability. But the benefits of new versions go beyond performance enhancements.

New features also come with using the latest version that promise to open up new possibilities for our web applications. Security updates also come along with using the current version. And lastly, updated developer tools and workflows can increase development productivity.

Demo

In this demo project, we will look at two projects we have on GitHub to highlight the importance of Next.js performance. One is an optimized Next.js app, and the other is an unoptimized Next.js app.

In both projects, we created Home and About pages. We also included the current date, “See more…” buttons and an image:

Next.js performance demo project home page.webp

Building the unoptimized project

We created the pages.js file, the root page file of our app. Using the use client directive, we indicated that it is a client component—which is not a good decision.

We might have dynamically loaded the "SeeMore" component instead of importing it to be rendered conditionally. We also imported a few hooks from {next/navigation} that we subsequently decided not to utilize.

The next package was imported, and the moment package was installed and imported for a trivial function when it could have used the default date functions. Also, we used the image "img" and the anchor tags when we could have imported the "Link" and "Image" components.
And lastly, we forgot to remove the log to the console in our application. See the code below:

/*
 ./pages.js
*/
// not server rendered
"use client"

import { useState } from "react"; import SeeMore from "./components/SeeMore"; // unused packages import { useParams, useRouter, useSearchParams } from "next/navigation" // no specific imports import next from "next" import moment from "moment";

function getDate() { const momentDate = moment(); return momentDate.format('MMMM Do YYYY'); }

export default function Home() { const [seeMore, setSeeMore] = useState(false) console.log(seeMore) return ( <div className="mx-5"> <div className="text-red-700 my-7"> <a href="/">Home</a> <a href="/about" className="ml-3">About</a> </div> <h1 className="text-3xl font-bold">Welcome to Turing {getDate()}</h1> <div className="flex flex-col justify-center items-center"> <img src="turing image.png" alt="turing" className="w-[500px] h-[500px]" /> </div> <div> <button onClick={() => { setSeeMore(!seeMore) }} className="p-3 rounded shadow-xl border">See more...</button> {seeMore && <SeeMore />} </div> </div> ) }

Using the Google Lighthouse extension, we ran the performance test after running the command and making our project a production build.

npm run build && npm run start

Using Google’s Lighthouse, we ran a performance test and got the following result:

Next.js build Google’s Lighthouse, performance test result.webp

As we can see above, the performance is 90%.

The Next.js build result is as follows:

Next.js unoptimized build result.webp

Building the optimized project

In this optimized project, we imported the Image component instead of the img tag. We also created the SeeMoreButton as a client component to handle the state change. Note that inside the component, we imported the "SeeMore" component dynamically since it is rendered based on a condition.

import Image from 'next/image'
import SeeMoreButton from './components/SeeMoreButton'

function getDate() { const jsDate = new Date(); return jsDate.toLocaleDateString() }

export default function Home() { return ( <div> <div className="mx-5"> <h1 className="text-3xl font-bold">Welcome to Turing {getDate()}</h1> <div className="flex flex-col justify-center items-center"> <Image src="/turing image.png" alt='turing' priority width={500} height={500} /> </div> <SeeMoreButton /> </div> </div> ) }

We created the "getDate()" function to get the current date using the default "Date" object function instead of unnecessary installing and importing "moment.js". The links to the Home and About pages were modified using the "Link" component. And as a component, we exported it to be used on other components and pages. And as you can see below, we imported it into the "layout" file:

// ./components/Header.js
import Link from 'next/link'

export default function Header() { return ( <div className="text-red-700 my-7 mx-5"> <Link href="/">Home</Link> <Link href="/about" className="ml-3">About</Link> </div> ) }

// ./layout.js
import { Inter } from 'next/font/google'
import './globals.css'
import Header from './components/Header'

const inter = Inter({ subsets: ['latin'] })

export const metadata = { title: 'Next.Js Build Optimization', description: 'Ways to optimize build in Next.js', }

export default function RootLayout({ children }) { return ( <html lang="en"> <body className={inter.className}> {/* import Header component*/} <Header /> {children} </body> </html> ) }

Using the Google Lighthouse extension, we ran the performance test after running the command and making our project a production build.

npm run build && npm run start

Below are the results:

Next.js build Google Lighthouse extension test result after command.webp

We increased performance to 98% by creating an optimized production build. Here is the Next.js build result:

Next.js optimized build result after build command.webp

Conclusion

In the course of this article, we have delved into the essential topic of optimizing build performance in Next.js. We have explored a wide array of techniques, from using Next.js built-in optimization features, server-side rendering, static generation, caching, serverless functions, and more.

By utilizing these techniques, developers can be assured that Next.js applications are built for peak performance. Summarily, optimizing build performance in Next.js is not just a best practice but a necessity in this competitive world of web applications.

Web users expect speed and responsiveness at all times. So it’s imperative that developers adopt any tools and techniques that ensure their expectations are not only met but exceeded.

Author

  • Optimizing Build Performance in Next.js

    Theodore Kelechukwu Onyejiaku

    Theodore is a full-stack developer, technical writer, and course author. He gives back to the community through writing and open source.

Frequently Asked Questions

Next.js is a React.js framework used for building web applications. Optimizing its build performance is very important to achieve a better user experience, quick loading times, SEO, and more.

Server-side rendering reduces the load on the client by rendering the UI on the server. This can lead to improved performance by reducing data fetching, bundle size, and initial page load times.

Next.js provides built-in optimization features like image optimization and font optimization. These features help in serving optimized images and fonts, reducing load times, and improving JavaScript performance and visual stability in web applications.

Developers can employ the following strategies:

  • Reading the official Next.js documentation.
  • Keeping an eye for the release notes for each new version.
  • Participate in community forums and discussions.
  • Follow Next.js on their social platforms.

Lazy loading involves deferring the loading of certain components or libraries until they are needed. This helps reduce the initial loading time and improves performance by loading only what is necessary.

View more FAQs
Press

Press

What’s up with Turing? Get the latest news about us here.
Blog

Blog

Know more about remote work. Checkout our blog here.
Contact

Contact

Have any questions? We’d love to hear from you.

Hire remote developers

Tell us the skills you need and we'll find the best developer for you in days, not weeks.