How to easily reduce the size of your NextJS bundle by 30%

Team member
Adrien
CTO & Lead Developer
The core performance of NextJS is pretty good. A lot of optimizations are run in the background without users noticing. However, as the project develops, the size of the bundle increases in size. Next will always make sure the bundle size is minimal, but can't fix a bad implementation on its own.

On the grounds side, the bench may vary depending on the size of the application. In particular, this article will focus on importing Lodash, updating dependencies, properly importing certain methods, and optimizing component imports and dependencies.

So the end goal is to reduce the size of the bundle and to have an application as small as possible to please both users and robots.

1. Make an inventory

Before proceeding with any optimization, it is best to start with an inventory. This will not only help you measure your efforts, but it will also help you justify the time spent on a task that might seem useless to the untrained eye.

For example, I chose an internal Coteries project that had not yet been optimized. The project is built using NextJS 12, Chakra-UI, Fontawesome and contains many other dependencies such as Lodash, DayJS, Mixpanel, Google Analytics...

To analyze and visualize my bundle, I used the following libraries and packages:

  • Next-Compose-Plugin, which helps manage the plugins on the Next configuration file
  • next-bundle-analyzer, which allows you to visualize the bundle in order to see what is taking up space.
  • source-map-explorer, which helps to visualize the contents of the bundle more precisely. This package can be installed globally.

Below is the NextJS configuration file. Source maps must be activated in order to be able to perform the various analyses. Remember to deactivate them once the investigations are over!

const withPlugins = require (“next-compose-plugins”);
const {withPlausibleProxy} = require (“next-plausible”);
const withBundleAnalyzer = require (” @next /bundle-analyzer “);
const nextTranslate = require (“next-translate”);

const PlausiblePlugin = withPlausibleProxy;
const BundleAnalyzer = withBundleAnalyzer ({
enabled: process.env.analyze === “true”,
});

const NextConfig = {
productionBrowserSourceMaps: process.env.analyze === “true”,
reactStrictMode: true,
pictures: {
formats: ["image/avif”, “image/webp"],
domains: ["***"],
},
};

module.exports = withPlugins (
[PlausiblePlugin, BundleAnalyzer, NextTranslate],
NextConfig
);

To use the next-bundle-analyzer library, we need to add the following command in package.json: “analyze”: “analyze=true next build”.

It is now possible to execute the following commands to get an initial reference:

  • PNPM Run Build: builds the project and gives information about the first JS load.
  • pnpm run analysis: gives a general idea of the distribution of the bundle.
  • source-map-explorer.next/static/**/*.js: gives more detailed information about the package.

Here are the numbers in my case:

  • 2.23 MB for bundle size with analyze
  • 2.08 MB for bundle size with source-map-explorer
  • 656 kB uploaded with 1.9 MB of resources on Network Inspector

In addition to these numbers, I ran the tests web.dev/measure 5 times to get an average performance score. This figure will help us see if we are making real improvements or if our efforts are in vain. The initial average score on web.dev is 69.2

Coteries Agence Digitale Reduce Bundle web.dev:measure tests 5 times

2. Improving Lodash imports

Lodash is a fairly standard library, and the chances of it being used in your project are quite high. With its 46 million downloads per week, the library is very popular, and using it correctly is becoming essential.

The following image shows how optimizing imports can have a significant impact on file sizes. We observe a factor of 10x depending on how to import methods!

import _ from “lodash”;//73.13kB (gzip: 25.43kB)
import {isEmpty} from “lodash”;//73.13kB (gzip: 25.43kB)
import isEmpty from “lodash/isEmpty”;//7.04kB (gzip: 2.26kB)

Of course, it is the last method that is recommended

We used the second method in this project. Editing the imports is fairly quick, and it only took 5 minutes to make the change.

  • 2.17 MB for bundle size with analyze (☺️ -2.691%)
  • 2.02 MB for bundle size with source-map-explorer (☺️ -2.885%)
  • Still 69.2 as an average performance score
  • 632 kB loaded with 1.8 MB of resources on the network inspector (☺️ -3.659%)

The changes are quite small, but so is the effort to make them happen. That's why it's worth taking the time to make this improvement.

3. Use dynamic imports

Next supports ES2020 dynamic imports. This means that it is possible to dynamically import components that are not shown by default.

A modal, an error warning, and a function triggered solely by user interaction are all items that are not required by default. Dynamic imports offer a simple way to manage these cases.

The rule is pretty simple. Anything that is displayed conditionally can be imported dynamically. Look for all the {VARIABLES & &... in your code, and you should see the components that can be changed.

const ServiceBadge = dynamic () => import (“.. /Molecules/ServiceBadge”));
const serviceHeader = ({service, title}: Props) => {
const router = useRouter ();
const isMobile = useIsMobile ();
const keys = textStore ((state) => state.keys);
Return (
<HStack>
<backButton onClick= {() => router.replace (ROUTE_ROOT)}///
{! IsMobile &} <ServiceBadge icon= {service.icon} color= {service.color} />
<Heading>{title}</Heading>
</HStack>
);
};

In this case, the ServiceBadge will only be loaded if the user consults the site from their mobile

Also, look at the methods that are run on user inputs. Import what they need inside the methods instead of at the top of the file. This way you'll only load methods when you need them instead of loading them every time.

const handleLogout = async () => {
await subabase.auth.signOut ();
setUser (null);
const showToast = await import (“../.. /utils/showToast “) .then (
(mod) => mod.showToast
);
showToast (keys.logout_success, “success”);
};

We only load the toast when the user clicks the logout button

  • 2.21 MB for bundle size with analyze (+1.843%)
  • 2.06 MB for bundle size with source-map-explorer (+1.980%)
  • 72 as the average performance score (+2.2)
  • 568 kB uploaded with 1.6 MB of resources on the network inspector (🤯 -10.127%)

The dynamic import of components has a huge impact on the size of the JS loaded when the website is opened. This simple modification made it possible to reduce the initial load by 10%, which is quite significant.

This reduction alone explains why the performance score improved by more than 2 points. Using dynamic imports definitely helps, and the result will be even more significant with a more complex page.

4. Analyzing the Bundle

This step can have the biggest impact, and it can also take the longest because it depends on many factors. Thanks to the bundle-analyzer and source-map-explorer, we have a clear view of what's going on in the bundle and what could be changed.

There are two things to look at at this point, whether there is a large imported file and whether there are a lot of small files. I don't know how much an optimized bundle should weigh or how many files it should contain. Certainly these two numbers should be as low as possible.

During my investigation, I discovered that some libraries were expensive and that react-syntax-highlight had a lot of files that took up a lot of space. You can see them in the following screenshots.

capture d'écran analyse du bundle

The Validator library is definitely taking up too much space for its use and react-syntax-highlight seems suspicious!

The analysis made me understand that I should look at some libraries: validator, framer, mixpanel, fontawesome, and react-code-blocks.

I wouldn't go into too much detail, but here's a quick summary of what I did for each bookstore:

  • validator: I changed the way I import methods and I use lodash isEmpty instead of the validator one.
  • framer: I did a few animations in vanilla javascript for the more specific elements and I kept everything as is for the more complex elements.
  • mixpanel: I removed it from the dependency since the data was not used, and we had other tools for that
  • fontawesome: unfortunately I wasn't able to reduce the size of the imports
  • react-code-blocks: I removed it from the library and directly used react-syntax-highlight instead since it was possible to import only what was needed.

In addition to these actions, I did a bit of cleaning up the libraries and replaced them with vanilla TypeScript because they didn't bring much improvement. I also took the time to update each package to the latest version in the hope of significant gains (thanks to Ncu).

Finally, I did a code analysis and removed the old and unused code. This step does not necessarily lead to a smaller bundle, but it is definitely positive for code quality and ease of use.

  • 1.59 MB for bundle size with analyze (🔥🔥 -28.054%)
  • 1.45 MB bundle size with source-map-explorer (🔥🔥 -29.612%)
  • 73 as an average performance score (+1)
  • 489 kB loaded with 1.3 MB of resources on the network inspector (🔥🔥 -13.908%)

I knew that this step would be the one that would bring the most results, but I did not expect such improvements!

Removing 30% of the bundle size by simply making smarter library selection choices seems crazy.

5. Don't use server data in useEffects

Next offers the possibility of retrieving data from the server and transmitting it to the client. This makes it possible to speed up the loading of the page since the server bandwidth is probably faster than your Internet connection. In addition, having the data when the page loads helps to reduce the number of network calls.

Next also offer the possibility of carrying out a static generation by increment. This is a function that I highly recommend, as it allows for an almost instant loading of the page while ensuring that the data is fresh.

Getting data from the server is a great thing. On the other hand, it would be a shame to use this data and manipulate it directly on the client side.

This means that Next will not be able to compile the page on the server. Some components will be loaded on the client side, causing the layout to lag (Layout Shift) or poorer performances.

Let's take the following example: we retrieve items from the server and then regenerate the page in increments every 15 minutes (thanks to revalidate: 900).

However, we have the useEffect method whose execution is guaranteed on the client and which defines a state where the item is saved. This is pretty bad because Next won't be able to build and pre-generate this page.

export const getStaticProps: getStaticProps = async () => {
const exampleArticles = await fetchExamplePosts ();
return {
About: {
ExempleArticles,
},
revalidate: 900,
};
};
const DisplayArticles = (exampleArticles: Props) => {
<ExamplePost [] >const [examples, setExample] = useState ();
useEffect () => {
setExample (exampleArticles);
}, []);
Return (
<div>
{examples &
examples.map ((post) => (
<>
<img src= {post.image.url} alt=”” />
<p>{post.name}</p>
<>
))}
</div>
);
};

Instead, here is what should be done: access the data directly in the function return, and the article will then be present when the page is built on the server.

export const getStaticProps: getStaticProps = async () => {
const exampleArticle = await fetchExamplePosts ();
return {
About: {
exampleArticle,
},
revalidate: 900,
};
};
const DisplayArticles = (exampleArticles: Props) => {
Return (
<div>
{! isEmpty (exampleArticles) &
exampleArticles.map (post) => (
<>
<img src= {post.image.url} alt=”” />
<p>{post.name}</p>
<>
))}
</div>
);
};

One last point: note that it is normal for some data to be manipulated on the client side. However, there are cases where data is retrieved from an API and displayed (as is the case in the examples above).

In these cases, be careful not to manipulate the client-side data and display it directly if it is already present when the pages load.

To conclude

The following table shows the impact of our various actions on the size of the bundle or on the performance score. Each step has a different impact and the results you can achieve will vary considerably.

Tableau représentant la taille du bundle

Two actions stand out for the most part: using dynamic imports for user interface elements that are not displayed by default, and looking for libraries that add weight to the bundle.

Dynamic imports are a great feature of NextJS, and they allow less JavaScript to be downloaded. It was possible to reduce the amount of JavaScript downloaded when the home page was loaded by 64 kB, and the difference may be even more significant for more complex pages that cause the page to load almost instantly.

Analyzing the bundle is by far the task that took me the longest. Replacing or correcting certain libraries can have significant impacts and require the refactoring of large parts of the code, which makes things more difficult. Stay rational and don't spend countless hours trying to remove a library that's essential to your project.

I must admit that improving Lodash imports will have had a significant impact. In reality, the impact on size will depend mainly on the importance of using the library in your project. Plus, it's a quick job, and it would be a shame not to keep the old imports!

Capture d'écran Résultat final de la compilation

Final result of the compilation. The result may not be very impressive but users seem to like it!

In any case, the research I did was fascinating and allowed me to understand a lot of things about Next and bundling.

It was a fantastic learning experience that will benefit both our development team and to our customers. I can already see what we can improve to provide even faster websites!

Hopefully you will be able to reduce the loading time of your website.

All the code for the screenshots can be found in this gist.

Let's work together!

Tell us about your project or need, without commitment ! Of course, we guarantee the highest confidentiality.
Team member
Sébastien
Co-founder
Thank you! Your submission has been received!
Oops! There was a problem with this form and we will correct it as soon as possible.

In the meantime, please send us your request by email to info@coteries.com.

See you soon!
By clicking Send, you agree to our terms of use and our privacy policy.