Next.js is an open source React framework that helps developers rapidly build high-performance web applications with excellent user experience and SEO benefits. It’s trusted by leading brands such as Netflix, Uber, and Starbucks.
In this blog post, we’ll share some steps you can take to improve your performance when using Next.js.
Improving the performance of Next.js apps
Next.js comes with image optimization, hybrid static and server-side rendering, automatic code-splitting and bundling, JavaScript and CSS optimization, and more. But even with an optimized framework, as you scale your site and apps, it becomes more crucial to leverage every useful feature in the framework. Here are a few built-in features and some widely accepted best practices that you could use to significantly improve your website’s performance.
Dynamic imports
When you let applications load all the JavaScript and CSS together initially, it significantly increases load times and negatively affects user experience. Instead of serving everything for that page all at once, including the components that the user might not need, Next.js supports dynamic imports which allow you to split the code into smaller chunks and load them only when needed. This also helps with reducing the size of the overall application.
There are two ways you can do this code-splitting in Next.js.
Route-based splitting: By default, Next.js splits JavaScript into manageable chunks for each route. As a user uses your app, they interact with different UI elements on the page, making Next.js send the chunks of code associated with each route. This reduces the amount of code that needs to be parsed and compiled at once.
Component-based splitting: It's possible to optimize even more on a component level. If your app has large components, you can split them into separate chunks. This means any components that are not critical or only render when the user interacts with the UI can be lazy loaded as needed.
Optimizing third-party scripts
When you use third-party scripts like Google Analytics, they sometimes draw extensively from the main thread’s precious resources. This means more render-critical components are blocked because of these third-party scripts. Next.js comes with a next/script component that lets you prioritize when to load these scripts. Using Next.js with Partytown or a visual development platform like Builder will help you optimize your JavaScript while also adding flexibility.
We’ve covered these code improvements in more detail in our ultimate guide to optimizing JavaScript.
Optimizing images
We’ve tackled this before in our definitive guide to image optimization. Next.js comes with a powerful and flexible image optimization component that does all of this automatically. To improve your performance, here's a quick list of the ways Next.js helps optimize your images:
- Prioritizing the images you want to load, and only loading what’s required. Images above the fold can be loaded first. Images below the fold can be loaded asynchronously when the user scrolls down. The Next.js image component will do this by default.
- Serving the right size of images in the right formats depending on the user’s bandwidth and resources. The Image component will automatically optimize your images.
- Resizing images as needed when you use responsive images
- Supporting next-gen formats, like WebP
- Avoiding cumulative layout shifts by using placeholders until images loads
Additional optimization
You could also optimize your CSS for improved performance. Next.js comes with built-in CSS support. We’ve addressed CSS-related best practices in our complete guide to optimizing CSS. Here’s a handy checklist to help you optimize your styles:
- Reduce CSS file size by removing unused code and concatenating, minifying and compressing your CSS.
- Prioritize loading render-critical CSS before the rest by inlining critical CSS in HTML, avoiding @import, and lazy loading CSS as required.
- Avoid CSS in JavaScript, as this bloats your JavaScript and makes your pages load slower.
- Optimize your fonts using swap, fallback, and optional with font-display.
Next.js supports CSS Modules using the [name].module.css file naming convention. By using CSS Modules you can avoid css collisions and they can be imported anywhere in your application. In the example below if an error class existed in the global scope the CSS Module would take precedence.
components/Button.module.css
.error {
color: white;
background-color: red;
}
components/Button.js
import styles from './Button.module.css'
export function Button() {
return (
<button
type="button"
// Note how the "error" class is accessed as a property on the imported
// `styles` object.
className={styles.error}
>
Destroy
</button>
)
}
Incremental static regeneration (ISR)
Next.js pre-renders every page by default. It generates HTML in advance instead of using client-side JavaScript, which helps with performance as well as SEO. There are a couple of ways in which this pre-rendering is performed: static generation and server-side rendering. Next.js lets you choose from these options for each page.
For improved performance, static generation is the more effective option. In static generation, HTML is generated at build time and can be reused on each request. These pages need to be built only once and can be cached and served by CDNs without additional configuration, which increases the speed.
When you’re scaling your site, you sometimes need to create or update pages after your site is built. To use static generation on each page without rebuilding your entire site, you can use incremental static regeneration (ISR). When a page was pre-rendered at build time and is requested, the user will first see the cached version. In the meantime, Next.js will regenerate the page and, once the page is generated, invalidate the cache to show the updated page.
Instant personalization
While static pages built with Next.js are fast-loading and performant, everyone sees the same content. Sometimes you might need to personalize the pages for different audiences. However, when personalization is crucial, it usually comes with a performance trade-off. When a page is highly customized for a user, this might require several round trips to the server, the specific data unique to the user needs to be fetched and rendered on the fly, and JavaScript that blocks your main thread might add more content. All this makes your page load slower and leads to a poor user experience, especially on mobile.
You can solve this problem by using edge middleware, which allows you to cache the data and serve it from the CDN edge network. You can extract personalized information from the visitor's cookies, like their previous shopping history, purchases, or browsing behavior, which the edge middleware then uses to rewrite the page. In combination with incremental static regeneration from Next.js, these new parameters for the page are fetched and cached at the edge, so that it can be served instantly to subsequent users. This allows for high-speed personalization without any performance cost.
Conclusion
When you use a lightweight, performance-driven framework like Next.js, it’s critical to understand all the in-built components that you can leverage to increase your web page’s loading speed. We’ve noticed that optimizing JavaScript has the maximum potential for improving website performance. You can test this out for yourself by plugging your (or any) website into our Performance Insights tool.
Introducing Visual Copilot: convert Figma designs to high quality code in a single click.