Loading chunks on demand - beyond the router (part 2)
Tags: frontend, performance
Published: 2023-11-10 17:38:17

It's time to control when your application requests the chunks of your page and optimize the loading.

Let's look at how to split up the chunks and load them at the right time. I'll show it with a [personal project](https://frontend-pattern.vercel.app). The stack is as follows: 

- [React](https://react.dev/) (SPA);
- [Vite](https://vitejs.dev/);

The same is applicable (**changing the syntax, obviously**) to any other JS frameworks and bundler tool, consult the doc to know how.

NOTE: This is the second part, see [the first post here](https://dev.to/tassiofront/optimizing-page-load-with-reactlazy-beyond-the-router-3p3m)


## Reducing the size of the main chunks

Let's dig deeper into the main chunk files and explore how to reduce them. By the way, do you know who the main chunk files on SPAs (Single-page applications) are? Usually, tools such as Vite and Wepack create two main chunk files: index.hash.js and vendor.hash.js:

index: there are crucial application codes, such as App.tsx code;
vendor: All production dependencies (package.json > dependencies);

Why are they referred to as 'main' files? Both the index and vendor files are the first to load and are essential in all Single Page Application (SPA) routers. That means all code included (or not) in them will impact the page load. 

Note: [Vite has removed this behavior since the 2.9 version](https://vitejs.dev/guide/build.html#chunking-strategy). Now, by default, the index also has the production dependencies code. Even though Vite doesn't split them by default, it provides a plugin to do it for you:

```TS
import { splitVendorChunkPlugin } from 'vite';

export default defineConfig({
  plugins: [
    react(),
    splitVendorChunkPlugin()
]})
```

It's vital to keep your eyes on them and control their size, once those chunks tend to be the biggest of the application.


## Exploring the chunk sizes with rollup-plugin-visualizer

I’m using the [rollup-plugin-visualizer](https://www.npmjs.com/package/rollup-plugin-visualizer) to explore the chunk sizes of the project. This way, I can figure out if there are unnecessary chunks on them.

![exploring chunks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bfhhz120o3xek4guah8d.gif)

The Visualizer shows what the final bundle looks like, including each chunk size by hovering it. Seeking our main chunks (hovered on the gif):

- vendor.hash.js: 94.89kb gzip
- index.hash.js: 17.99kb gzip

On the same gif, it’s possible to see that the components folder is included on index.hahs.js. Some components in that folder do not need to be global, then it’s an opportunity to reduce the index.hash.js chunk size.

Seeking on the project, the TextInput component is used in some places and it has a unique import to the uniqid lib. 

![Seeking TextInput](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zolqp4zgsaku2wrgs7eu.gif)

If seek TextInput and uniqid, the first one is found on the index and the other on the vendor. It's amazing once it's possible to reduce index and vendor sizes.

![finding TextInput and uniqid on the graph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f05bqhbejdxhf1h1alb7.gif)


## Importing TextInput (and uniqid) dynamically

So, what is the strategy? Once TextInput is not global, there's no need to retain its code in the index chunk. Let's utilize the [Lazy API](https://react.dev/reference/react/lazy) to extract its code from there. Additionally, since uniqid is imported only in TextInput, it will be included in the new chunk and removed from the vendor chunk.

It's necessary to change all TextInput imports to make it work. **Each** import should use the Lazy API (n this case, there are 3 TextInput imports):

```TS
// import TextInput from '@/components/TextInput/TextInput'; // current code

// using lazy API
import { lazy } from 'react';
const TextInput = lazy(
  async () => await import('@/components/TextInput/TextInput')
);
```

Note: I have reduced the whole code to teaching purpose, but at the bottom, you can find the commit and the full source code. Read the comments to understand.

Once done, let's see the final bundle.

Now, TextInput has its chunk and it's no longer included on the index chunk. Additionally, the uniqid was also included on the new chunk and removed from the vendor chunk.

- vendor.hash.js: 93.24kb gzip (**1.65kb less size, no longer has uniqid code**)
- index.hash.js: 17.24kb gzip (**0.75kb less size, no longer has TextInput code**)
- TextInput.hash.js: 2.48kb gzip (new chunk with TextInput and uniqid)

Now, TextInput and uniqid will be downloaded, parsed, and compiled by the browser only when they are required. It's possible to see this on the Network tab, where the About router does not use TextInput.hash.js chunk, but Login does:

![network tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvtahbows7alohtky39r.gif)


## Conclusion

Well, you might be wondering: is it worth it for just a 2.4kb reduction in code? Indeed, this is just an example, and we can extend our exploration to other components beyond TextInput. However, it's essential to recognize that good performance results from a combination of improvements that collectively enhance the overall user experience of your application. Additionally, these concise examples in the code serve as reminders for teammates to continuously seek opportunities for code optimization.


- [Source Code here](https://github.com/tassioFront/frontend-pattern)
- [The commit with those changes](https://github.com/tassioFront/frontend-pattern/commit/57b3f6141a1fe4a2e466ee6ade49654780c29323)

[See my other articles](https://frontend-pattern.vercel.app/articles)