How to Improve Next.js App Performance with Code Splitting and Lazy Hydration

This article was written by Anel Kalajevac, Technical Lead at HTEC Group

When we talk about web applications, everything comes down to performance. Except for the user experience, web application performance has an impact on bounce rates, conversion rates, and ranking in organic search. In other words, slow web applications will hurt your brand and cost you money. There are many reasons why a web application may load slowly and many different approaches to improve performance. I’m going to show you how to improve the performance of Next.js-based applications by using the code splitting technique and lazy hydration. But first, let’s see what affects web application performance. 

What affects web application performance? 

The most common reasons why web application load time might be lagging are: 

  • Heavy JavaScript usage 
  • Large image sizes 
  • Not using browser cache 
  • Too many plugins and external libraries 
  • Slow network connection 
  • Older browsers 
  • Poor hosting plan 

I’m going to focus on heavy JavaScript usage, but first, let’s see how to test web application performance. 

How to test web application performance? 

Many different tools can be used for checking web application performance, but all of them are measuring mainly the same metrics. Alongside many different metrics, the most important are Largest Contentful Paint, First Input Delay, and Cumulative Layout Shift. These metrics are also defined as Core Web Vitals by Google. 

For web application performance checking I prefer using PageSpeed Insights, which provides a nice report for mobile and desktop devices. The report includes a general score calculated by Lighthouse Scoring Calculator, results for different metrics, and suggestions for improvements.  

The metrics scores and the performance score are colored according to these ranges: 

  • 0 to 49 (red): Poor. 
  • 50 to 89 (orange): Needs Improvement. 
  • 90 to 100 (green): Good. 

Now, when we know how to test web application performance, let’s see how it can be improved. 

Example 

Let’s say that we have a web application created by using React and Next.js framework. PageSpeed Insights reports two main issues:  

  • Reduce unused JavaScript. 
  • Minimize main-thread work. 

Idea 

Reducing unused JavaScript by splitting the main JavaScript file shared by all pages could be a good starting point, but can we go one step further? Do we need all that JavaScript during the initial load of the page? Why download the footer code if the user during the initial page load can’t see the footer? 

So, the idea is to load only JavaScript code relevant to the visible part of the page, and the rest will be loaded as the user scrolls down. This will reduce unused JavaScript and minimize main-thread work. 

Solution 

There are five core parts of implementation: 

  1. A webpack plugin for changing module ids. 
  2. Next.js plugin for marking lazy modules. 
  3. Custom Next.js document. 
  4. Next.js dynamic import. 
  5. Component for lazy hydration (library or custom implementation based on Intersection Observer API). 

First, I will create a webpack plugin to change the module id. 

Then, I will create a Next.js plugin to mark target modules as lazy and merge a webpack configuration with the Next.js configuration. This plugin will find defined modules for lazy loading and add the prefix “lazy” to the module id. For this purpose, I will use the previously created webpack plugin for changing module ids. The prefix “lazy” is an indicator that the module should be lazy-loaded when it is necessary. In the example below, I’ve defined the footer component as a lazy module. 

Now, when I have a logic that marks the footer component as a lazy module, the next step is a modification of Next.js internals within _document.tsx. I will extend next/head to skip printing <script> tags for lazy JS chunks into a document. Also, I will modify NextScript to let a webpack manage the loading of dynamic ids. 

The mechanism for excluding configured lazy chunks from the initial page load is implemented, so the last step is the dynamic import of configured lazy module (component) with a lazy hydration wrapper. Dynamic import is just an extension of React.lazy that helps us with code splitting. By using dynamic import, the footer component in our case will not be included in the page’s initial JavaScript bundle. For lazy hydration, I can use some of the existing libraries or create a custom solution called LazyHydration based on Intersection Observer API. 

As soon as I scroll near the footer component, webpack will download the dynamic import and LazyHydration will hydrate it into interactive react code. 

Summary 

Optimizing web application performance is a challenging undertaking that depends on speed test results and reported issues. Code splitting and lazy hydration can reduce your page weight and improve performance if you have issues with heavy JavaScript usage. When optimizing web application performance, the most important thing before taking any steps is looking through speed test results and focusing on high-impact factors.