31 October 2023
General strategies for optimizing performance in Next.js applications

Blazity

Next.js provides built-in performance enhancement features. However, to truly maximize its potential and boost core web vitals, there's more work involved. In this article, we'll explore how to optimize performance in Next.js apps and delve into popular techniques for further improvements.
Wojtek Wrotek
Wojtek Wrotek
React/Next.js Frontend Developer
General strategies for optimizing performance in Next.js applications

Performance metrics

Measuring performance in web apps using tools such as Google Lighthouse provides developers with insights into how well a site adheres to best practices. Lighthouse offers detailed feedback on performance, accessibility, and SEO, helping teams optimize their sites for both user experience and search rankings. This Performance score is calculated by combining several different performance metrics.

Metric

DescriptionWeight (Lighthouse 10)

First Contentul Paint (FCP)

The time it takes for the first piece of content to appear on the screen from when the page starts loading. It gives an idea about the perceived loading speed for users.10%

Largest Contentful Paint (LCP)

The time it takes for the largest content element (like an image or a text block) on the page to become visible. It's a measure of perceived loading speed and user experience.25%

Total Blocking Time (TBT)

Measures the amount of time that a page is blocked from reacting to user input, like a mouse click.30%

Cumulative Layout Shift (CLS)

Quantifies how often users experience unexpected layout shifts — a low CLS indicates a more stable interface. It's a core metric for understanding visual stability.

25%

Speed Index (SI)

Represents how quickly the contents of a webpage are visually populated. A lower Speed Index indicates a faster visual display10%

It's crucial to understand that Lighthouse scores might not always be accurate. Some techniques can skew or falsify the results. We recommend using tools like Page Speed Insights for more reliable feedback. Keep in mind, when using the Lighthouse tool in a browser, the performance can be influenced by the power of your computer.

Code splitting

Code splitting is an optimization technique that uses the next/dynamic feature, built upon React.lazy() and suspense. Instead of sending a large JavaScript bundle to the client, code splitting breaks it into smaller chunks, delivering only the necessary code when it's needed (Next.js does it for each page by default). This can speed up initial load times and offer more efficient caching. However, it’s worth noting that using dynamic imports too frequently can be ineffective. 


Use dynamic imports when:


  • Dealing with large libraries or components not needed immediately.
  • Rendering components conditionally (e.g., modals).
  • Integrating third-party tools or widgets occasionally.


Each dynamic import results in an extra server request, potentially leading to delays. Striking the right balance is crucial for maintaining peak performance.


Note: Dynamic imports and code splitting in Next.js (and Webpack) work on a file basis. Ensure individual components you intend to load separately are in distinct files. Otherwise, it’ll not work. It works the same way if you export components from one file (the technique used to make imports cleaner).

Progressive / selective hydration

Hydration is a process in web development where the client-side JavaScript brings to life the static content rendered by the server. When using techniques like server-side rendering in Next.js, hydration becomes crucial.


Here's a simpler breakdown:


  • Server-Side Rendering (SSR): The server sends your landing page's complete HTML to the browser. This means users can see the page even before any JavaScript runs.
  • JavaScript Comes into Play: After the page is displayed, the browser downloads and runs the related JavaScript. A lot of this JavaScript often just reinforces what's already in the HTML.
  • The Hydration Step: Now, this JavaScript starts adding interactivity to the page by connecting event listeners and creating a DOM structure. This step, turning a static page into an interactive one, is called hydration.


But there's a catch. Hydration can slow things down by blocking the main thread, which can increase the Total Blocking Time (TBT). So, while hydration makes a whole page interactive, it can also make it feel a bit slower for a moment. To combat this, some developers use progressive or selective hydration to only activate the most crucial parts of a page, improving both speed and user experience. There’s a cool library next-lazy-hydrate which helps to optimize this process. Basically while using this library you can simultaneously tap into the benefits of both code splitting and progressive hydration. It’s also worth noting that in Next.js 13 app directory you can achieve similar results by streaming server components.

Optimizing images

Using Next.js greatly simplifies image optimization with its next/image component. This component automatically configures width, height, and blurDataURL values for imported images, minimizing Cumulative Layout Shift (CLS) before the image fully loads. You can also set these values manually. The component compresses images for better performance, reducing the data browsers need to fetch. It also supports lazy and priority loading, allowing control over which images load first. This results in faster website load times and an enhanced user experience.


So in general it’s a good choice to use next/image in most cases. However, if your app has a lot of images, using that feature on some hosting providers can get expensive (they are optimized during runtime). Remember, next/image isn't the only way to optimize images in Next.js. While it's user-friendly and makes maintaining your app easier, you can always use other standard methods for image optimization such as:


  • lazy loading below the fold
  • setting fetchpriority for critical images
  • decoding images asynchronously
  • setting srcset manually


In fact, next/image uses these techniques under the hood but requires much less config.


Note: If you're using remote images and your Next.js version is below 12.3, you'll need to define domains in the Next.js config file. If you aim to use images from multiple domains, consider trying our next-image-proxy, which facilitates this.

Optimizing fonts

Next.js's next/font feature streamlines font optimization. It uses the CSS size-adjust property to prevent unexpected layout shifts when loading fonts. Additionally, it has a built-in method for using Google Fonts: instead of fetching them via external requests, Next.js downloads the font and CSS files during the build and bundles them with other web assets like HTML and CSS during deployment. However, as with next/image, there are manual methods to boost the performance such as:


  • font-display: swap
  • using system fonts instead of external ones
  • preloading fonts

Optimizing third-party scripts

Next.js's next/script component offers targeted strategies for loading third-party scripts to optimize performance:


  • beforeInteractive: Loads the script before Next.js initializes, potentially adding to initial rendering time but ensuring script availability.
  • afterInteractive (default): Loads the script after some page hydration, balancing between immediate functionality and smooth rendering.
  • lazyOnload: Waits for browser idle time to load the script, ideal for non-immediate scripts, enhancing perceived performance.
  • worker (experimental): Loads the script in a web worker, offloading execution to keep the main browser thread responsive. While promising, it's still in the testing phase (it uses Partytown under the hood). 


It also offers enhanced flexibility by allowing users to integrate callback functions like onLoad, onReady, and onError. These callbacks can be crucial in handling script behaviors and responding to different loading scenarios. To dive deeper and understand its full capabilities, refer to the official Next.js documentation.

Bundle Analysis

In Next.js, code is segmented into modules and bundled by webpack into single files for execution in the browser. As apps grow in complexity, their codebase can become bulky, affecting load times and maintainability. Analyzing these bundles helps pinpoint optimization opportunities.


To enhance performance, consider:


  • Eliminating duplicate or unused npm packages.
  • Applying minification, tree shaking, and code splitting for JavaScript files.
  • Moving some dependencies to the server bundle


For developers looking to enhance the performance of their Next.js apps, bundle analysis is an essential step. The @next/bundle-analyzer is a Next.js library designed to inspect the size and structure of your app's bundle. It helps pinpoint bulky code segments, allowing you to trim down the bundle size and improve the performance.

Conclusion

Optimizing performance for Next.js applications is essential but can be complex. Luckily, Next.js provides tools to help streamline this task. For larger projects, it's important to plan optimization by declaring performance budget, since not all techniques will suit every context. For those who want more detailed information, we'll be preparing an article focused on how to set and stick to a performance budget. With proper planning and the right tools, boosting performance becomes easier, leading to more satisfied users.


blazity comet
Get a quote
Empower your vision with us today
The contact information is only used to process your request. By clicking send, you agree to allow us to store information in order to process your request.