The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Optimizing performance in production environments can sometimes be an uphill task. Fine-tuning site performance cannot be overlooked, as the demerits cause slow web pages with bad UX. These sites tend to load slowly, have slow-rendering images, and in the long run, lead to increased bounce rates from site visitors as most users won’t be willing to wait for content to pop up.
In this tutorial, we will cover different patterns to speed-up site performance in a Next.js application.
At the end of this article, readers will have a clear understanding of how they can maximize performance in a Next.js web application. We’ll cover the following:
To follow along with this article, prior knowledge of the Next.js framework is required.
Dynamic imports, also known as code splitting, refers to the practice of dividing bundles of JavaScript code into smaller chunks, which are then pieced together and loaded into the runtime of an application as a means to drastically boost site performance.
It was developed as an upgrade to static imports in JavaScript, which is the standard way of adding imports for modules or components at the top level of a JavaScript module using the imports syntax.
While this is a commonly used method, there are some drawbacks where performance optimization is concerned, especially in cases such as:
Unlike static imports, dynamic imports work by applying a method known as code splitting. Code splitting is the division of code into various bundles, which are arranged in parallel using a tree format, where modules are loaded dynamically — the modules are only imported and included in the JavaScript bundle when they are required. The more the code is split, the smaller the bundle size, and the faster the page loads.
This method creates multiple bundles that are dynamically loaded at the runtime of the webpage. Dynamic imports make use of import statements written as inline function calls.
Let’s look at a comparison. Suppose we wish to import a navigation component in our application; an example of static and dynamic imports for a navigation component is illustrated below:
Static import:
import Nav from './components/Nav'
export default function Home() {
return (
<div>
<Nav/>
</div>
)
}
Dynamic import:
import dynamic from "next/dynamic";
import { Suspense } from "react";
export default function Home() {
const Navigation = dynamic(() => import("./components/Nav.js"), {
suspense: true,
});
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Navigation />
</Suspense>
</div>
);
}
Here, the navigation component has its relative part specified in the import() block. Note that next/dynamic does not allow template literals or variables to be used in the import() argument.
Also, react/suspense has a specified fallback element, which is displayed until the imported component is available.
Optimizing site performance as a result of implementing dynamic imports will, in turn, result in the following site benefits:
With all of these benefits, you are probably thinking about how to use dynamic imports in your application. Then, the big question is, how can we implement dynamic imports and code splitting in a Next.js application? The next section shows detailed steps on how you can achieve this.
Next.js makes it easy to create dynamic imports in a Next application through the next/dynamic module, as demonstrated above. The next/dynamic module implements lazy loading imports of React components, and is built on React Lazy.
It also makes use of the React Suspense library to allow the application to put off loading components until they are needed, thereby improving initial loading performance due to lighter JavaScript builds.
Earlier in this article, we demonstrated importing a component using next/dynamic. But we can also make dynamic imports for functions or methods exported from another file. This is demonstrated as follows:
import React from 'react'
export function SayWelcome() {
return (
<div>Welcome to my application</div>
)
}
const SayHello = () => {
return (
<div>SayHello</div>
)
}
export default SayHello
In the code above, we have a component, SayHello, and a named import, SayWelcome. We can make a dynamic explicit import for just the SayWelcome method, as shown below:
import dynamic from "next/dynamic";
import { Suspense } from "react";
export default function Home() {
const SayWelcome = dynamic(
() => import("./components/SayHello").then((res) => res.SayWelcome)
);
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<SayWelcome />
</Suspense>
</div>
);
}
The above code imports the SayHello component and then returns the SayWelcome export from the response.
Suppose we have UserDetails and UserImage components. We can import and display both components as follows:
import dynamic from 'next/dynamic'
const details = dynamic(() => import('./components/UserDetails'))
const image = dynamic(() => import('./components/UserImage'))
function UserAccount() {
return (
<div>
<h1>Profile Page</h1>
<details />
<image />
</div>
)
}
const App = () => {
return (
<>
<UserAccount />
)
</>
}
export default App
In the code above, we added dynamic imports for the UserDetails and UserImage components, then we put these components together into a single component, UserAccount. Finally, we returned the UserAccount component in the application.
With the next/dynamic module, we can also disable server-side rendering for imported components and render these components on the client-side instead. This is particularly suitable for components that do not require much user interaction or have external dependencies, such as APIs. This can be done by setting the ssr property to false when importing the component:
import dynamic from 'next/dynamic'
const HeroItem = dynamic(() => import('../components/HeroItem'), {
ssr: false,
})
const App = () => {
return (
<>
<HeroItem />
)
</>
}
Here, the HeroItem component has server-side rendering set to false, hence it is rendered on the client-side instead.
Apart from importing local components, we can also add a dynamic import for external dependencies.
For example, suppose we wish to use Axios fetch to get data from an API whenever a user requests it. We can define a dynamic import for Axios and implement it, as shown below:
import styles from "../styles/Home.module.css";
import { React, useState } from "react";
export default function Home() {
const [search, setSearch] = useState("");
let [response, setResponse] = useState([]);
const api_url = `https://api.github.com/search/users?q=${search}&per_page=5`;
return (
<div className={styles.main}>
<input
type="text"
placeholder="Search Github Users"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button
onClick={async () => {
// dynamically load the axios dependency
const axios = (await import("axios")).default;
const res = await axios.get(api_url).then((res) => {
setResponse(res);
});
}}
>
Search for GitHub users
</button>
<div>
<h1>{search} Results</h1>
<ul>
{response?.data ? (
response && response?.data.items.map((item, index) => (
<span key={index}>
<p>{item.login}</p>
</span>
))
) : (
<p>No Results</p>
)}
</ul>
</div>
</div>
);
}
In the code above, we have an input field to search for user names on GitHub. We use the useState() Hook to manage and update the state of the input field, and we have set the Axios dependency to be dynamically imported when the button Search for GitHub users is clicked.
When the response is returned, we map through it and display the usernames of five users whose names correspond with the search query entered in the input field.
The GIF below demonstrates the above code block in action:

We learned about dynamic imports/code splitting, its advantages, and how to use it in a Next.js application in this article.
Overall, if you want to shorten the time it takes for your website to load, dynamic imports and code splitting are a must-have approach. Dynamic imports will significantly enhance the performance and user experience of your website if it has images, or if the results to be shown are dependent on user interactions.
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket captures console logs, errors, network requests, and pixel-perfect DOM recordings from user sessions and lets you replay them as users saw it, eliminating guesswork around why bugs happen — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.

Learn about the new features in the Next.js 16 release: why they matter, how they impact your workflow, and how to start using them.

Test out Meta’s AI model, Llama, on a real CRUD frontend projects, compare it with competing models, and walk through the setup process.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now