Go to homepage

Fix Lighthouse "Reduce unused JavaScript" using Next.js Bundle Analyzer

Problem

A Lighthouse report or a PageSpeed Insights report warns you that you should Reduce unused JavaScript (shown in the image above).

You probably end up doing a little bit of research, and you learn that you should use the webpack-bundle-analyzer to find out if there is any bloat you can remove from your JavaScript. The bundle analyzer opens the following visualization in your browser.

But, it can be confusing what to even do with this report. Maybe, you did not even make it this far. In this post, I will explain how to set up the bundle analyzer and take actionable steps to reduce your unused JavaScript by showing you the following:

  1. how to run a Lighthouse report on your Next.js website
  2. how to understand the Reduce unused JavaScript output
  3. how to install and run the @next/bundle-analyzer on your Next.js website
  4. how to actually reduce any unused JavaScript

The Sandbox

For demonstration purposes, I initialized a new Next.js website, and I installed three very large Javascript packages using yarn:

  1. Three.js (606kb minified)
  2. Material UI (493kb minified)
  3. Video.js (580kb minified)

I imported all three of these enormous packages into the index.tsx to simulate a Next.js website with some large dependencies:

I am using import * ... because I want to make sure this sandbox app imports as much JavaScript as possible, so I can trigger the Reduce unused JavaScript error in Lighthouse. In your actual webapp, you should only import what will actually be used.

In the next section, I will show you how to run a Lighthouse report on your Next.js website.

Find unused JavaScript using Lighthouse

You can use Lighthouse to find the largest JavaScript chunks that are downloaded on each page.

1. Build a production build of your Next.js website

npm run build

2. Start the production build of your Next.js website

npm run start

3. Open a New Incognito Window in Google Chrome

4. Open up the production version of your Next.js website (default: https://localhost:3000/)

5. Open the Google Chrome DevTools

6. Click on the Lighthouse tab at the top of the DevTools

7. Click on Analyze page load to generate a Lighthouse report for your website

8. Once the report has been generated, scroll down to the list of Opportunities, and expand the Reduce unused Javascript opportunity

In the above screenshots, you'll see three URLs for the largest JavaScript chunks that were downloaded when you loaded the website:

  1. …chunks/609-52e1ddd0e27842fe.js
  2. …chunks/d6e1aeb5-79251ebb86bd0684.js
  3. …chunks/fb7d5399-6342497ed0d1ca3c.js

The first chunk contained the following code:

(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[609],{9026:function(e,t,r){"use strict";let n,o,i,a,l,s,u,c;r.r(t),r.d(t,{Accordion:function(){return iH},AccordionActions:function(){return iJ},AccordionDetails:function(){return i3},AccordionSummary:function(){return aA},Alert:function(){return a0},AlertTitle:function(){return lp},AppBar:function(){return lw},Autocomplete:function(){return uR},Avatar:function(){return uB},AvatarGroup:function(){return uV},Backdrop:function(){return u4},Badge:function(){return cu},BottomNavigation:function(){return...

You can tell that this belongs to the MUI package because of the presence of the Accordion component. To be honest, it's really not clear because the code has been minified. It forces you to to decipher something very ambiguous. In the next section, we can use the bundle analyzer to figure out exactly which packages make up each chunk.

Investigate chunks using Next Bundle Analyzer

In this section, we will install and run the @next/bundle-analyzer package to examine the chunks listed in the Lighthouse report we generated above.

1. Install the @next/bundle-analyzer package as a dev dependency (dev dependency since we only need to use this tool during build time).

yarn add @next/bundle-analyzer --dev --save

2. Modify your project's next.config.js file to add support for the bundle analyzer.

const nextConfig = {
  ...
};

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer(nextConfig);

3. Run the bundle analyzer on the production build:

ANALYZE=true yarn build

4. The bundle analyzer will generate a file called client.html and open it up in your browser. You can ignore the nodejs.html, edge.html, or any other files that open. You'll be presented with a pretty visualization of colored blocks where the size of each block represents how much space it takes up.

In the above screenshot, I highlighted the the hashed chunk names and a popover showing the size for one chunk. These are the same chunks that we saw in our Lighthouse report.

Now, we can clearly identify which packages a chunk contains. For example, we can confirm that chunk 609-52e1ddd0e27842fe.js (the top-left block) contains the MUI package.

In the following section, I will go over ways that we can reduce the amount of unused JavaScript.

Reduce the amount of unused JavaScript

I've outlined some of the most common ways to address large chunk sizes.

  1. Make sure you only import code you need from packages, so tree-shaking can remove any unnecessary code
  2. Next.js prefetches pages when you use the Link component, but you can turn off prefetching (see link)
  3. Dynamically load packages so they are only downloaded when you need them. You can dynamically load a video player when a user opens up a modal. Your users only download the video player chunk when a modal is opened.

Make use of tree-shaking

For the sandbox, I have imported full libraries using the following syntax:

import * as MUI from '@mui/material';

This is never recommended, and I am doing this for demonstration purposes. This MUI package exports a lot of components, so there is no point in me importing all packages. If I only need to use a simple Badge component, the above syntax will also end up importing a Table component that is never used.

I will fix this universal import by only importing the Badge component:

// import * as MUI from '@mui/material';
import Badge from '@mui/material/Badge';

Dynamically load components

I will move the Three.js and Video.js imports into a separate component called BigImport and dynamically import the BigImport component using next/dynamic.

// index.tsx
const BigImport = dynamic(() => import('../components/BigImport'));

export default function Home() {
  const showBigImport = false;

  // render `BigImport` dynamically, so we only download it conditionally
  return (
    <>
      ...
          {showBigImport && <BigImport />}
      ...
    </>
  );
}
// components/BigImport.tsx
import * as THREE from 'three';

import videojs from 'video.js';

function BigImport() {
  const scene = new THREE.Scene();
  const player = videojs('');

  return <div>I am a component that imports large packages!</div>;
}

export default BigImport;

Next, let's run another Lighthouse report with the changes to prove that we reduced the size of the Javascript.

I also ran the Next.js Bundle Analyzer again to make sure that we are only importing the Badge component from the MUI library.

The above screenshot confirms that we are only importing the Badge component, and not any other components, in the MUI chunk.

Conclusion

The bundle analyzer can be an important tool to use in your quest to optimize your webapp's performance. I hope this post has helped you improve your Next.js website performance.