Leverage Turing Intelligence capabilities to integrate AI into your operations, enhance automation, and optimize cloud migration for scalable impact.
Advance foundation model research and improve LLM reasoning, coding, and multimodal capabilities with Turing AGI Advancement.
Access a global network of elite AI professionals through Turing Jobs—vetted experts ready to accelerate your AI initiatives.
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.
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:
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.
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.
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.
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 "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".
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 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.
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:
All these stated above, and many more, help to improve Next.js performance.
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 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.
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.
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.
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.
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 help identify bottlenecks, diagnose issues, and ensure that our application is running efficiently. These processes include:
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.
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.
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.
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:
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:
As we can see above, the performance is 90%.
The Next.js build result is as follows:
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:
We increased performance to 98% by creating an optimized production build. Here is the Next.js build result:
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.
Theodore is a full-stack developer, technical writer, and course author. He gives back to the community through writing and open source.