Back to Blog
Blazity

General strategies for optimizing performance in Next.js applications

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.

General strategies for optimizing performance in Next.js applications

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 Description Weight (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 display 10%

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 PageSpeed 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 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).

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.

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.

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

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.

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.

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.


Contact us today...

to discuss your web development project and discover how our team can help you achieve your goals.

Tell us about your project

Empower your vision, partner with us today

Author photo
Jakub Czapski
Founding Partner at Blazity
To best serve you, let's get to know your requirements first. Tell us about your project, and we'll arrange a free consultation call to discuss how we can help.

Trusted by:

Trusted by logoTrusted by logoTrusted by logoTrusted by logo
Do you need an NDA first?