Splitting and Caching React Chunks
Tags: javascript, webdev, react, programming
Published: 2023-12-30 16:42:41

> _There are only two hard things in Computer Science: **cache invalidation** and naming things._ - Phil Karlton

Caching is still today one of the hardest things to get right. Nevertheless, it stands as an indispensable element for optimizing performance in any system, essential for unlocking its full potential.

For a SPA (Single Page Application), the _perfect_ caching strategy would be to split our code into 2 different categories: **common chunks** and **route chunks**. That way, we could in theory only download the necessary JS/CSS code for a given route and re-use all the common (already cached), parts of the code.

## Code Chunks

![Code Chunks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q46i21d9pc6rsrp7f3fk.png)

A **"chunk,"** as defined by [Webpack](https://webpack.js.org/glossary/#c), refers to the distinct components that collectively form a bundle. In the context of [Webpack](https://webpack.js.org/), a bundle serves as the entry point for organizing and encapsulating these individual chunks of code.

Another property that distinguishes `chunks` is their independence, meaning that every one of them will be downloaded as a different source - e.g. a bundle with 4 chunks will lead to 4 `HTTP` requests.

## Code Splitting

To deliver the best experience in a web application, it is crucial that the browser only downloads and executes the bare minimum JS/CSS that's needed for a given page/route and that it progressively downloads and executes the rest as needed.

To achieve these sorts of optimizations, code bundlers (e.g.  Webpack, Rollup...) tend to provide a way to split chunks per route. This means that for each route visited, there will be a preset of chunks that will be downloaded by the browser, as the user navigates through the different pages/routes.

![Chunks Downloads](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mhdtkd31iw548duf3px7.png)

Above we can see a very simplistic view of how navigating into the `/page1` route would lead to only downloading the necessary parts - `common-chunk.js` and `page1-chunk.js`.

### Identifying the common code

For the most part, bundlers do a "good enough" work by creating multiple independent chunks that can be re-used across a web app, so that users only download the bare minimum that's needed for a given page. Take a quick peek at Webpack [docs](https://webpack.js.org/plugins/split-chunks-plugin/#defaults) below on when it creates a new chunk automatically:

``` 
Webpack will automatically split chunks based on these conditions:

- New chunk can be shared OR modules are from the node_modules folder
- New chunk would be bigger than 20kb (before min+gz)
- Maximum number of parallel requests when loading chunks on demand would be lower or equal to 30
- Maximum number of parallel requests at initial page load would be lower or equal to 30

When trying to fulfill the last two conditions, bigger chunks are preferred.
``` 

However, we can probably do better with very little work, just by looking at our app and understanding which parts of the code we should extract into independent chunks.

While there is potential to take loads of time to optimize this process, we should probably stick to the ones that will have the most impact on our bundle size and that will be used in most routes, and these usually fall under one of the following categories:

- [related vendors] `react`, `react-dom` and `react-router-dom`
- [globally used vendors] `lodash`, `css-in-js` lib, etc...
- [global styles] `CSS Reset` files, `global styles` files, etc...

### Extracting common parts into chunks

Once we have identified all the common parts of code we want to extract into independent chunks, it's usually very straightforward to actually set this up in whatever bundler you are using. 

Here is an example of how we could create a new chunk for all the `react` related vendors using `webpack`:

```js
const path = require('path');

module.exports = {
 ...,
  optimization: {
    splitChunks: {
      cacheGroups: {
        reactVendor: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'vendor-react',
          chunks: 'all',
        },
      },
    },
  },
};
```

This Webpack config uses `splitChunks` combined with `cacheGroups` to create specific code chunks that pass the given `test` regex:


```js
reactVendor: {
  test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
  name: 'vendor-react',
  chunks: 'all',
},
```

After we apply this change to the config, Webpack will generate a new chunk that will include any code that comes from `react`, `react-dom`, or `react-router-dom`, with the name `reactVendor.js`. 

The last step to get this working, is to include this newly produced asset in our HTML file:

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My App</title>
    <script src="./vendor-react.js"></script>
  </head>
  <body>
    <script src="./app.js"></script>
  </body>
</html>
```

Since we need `React` to run our application, it's better to include it on the `head` tag, so it's picked up faster by the browser. 

However, to fully leverage this new chunk, it's **crucial to configure caching**, ensuring it is downloaded only once and subsequently "reused" across all page navigations.

## Caching Static Assets (Chunks)

To efficiently cache static assets, there are certain aspects we need to take into consideration:

- what's the best strategy
- when to invalidate the cache
- how to invalidate the cache

In my opinion, the best way to cache static assets is a combination of **CDN distribution** with **cache busting**, so that's what we will discuss below.

### CDN Distribuition

![CDN Distrtibution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9p6ujt0ol7xcyotc3v9.png)

> _A content delivery network (CDN) is a geographically distributed group of servers that caches content close to end users. A CDN allows for the quick transfer of assets needed for loading Internet content, including HTML pages, JavaScript files, stylesheets, images, and videos._ - [Cloudflare](https://www.cloudflare.com/en-gb/learning/cdn/what-is-a-cdn/)

The CDN will be 1st opportunity for caching, since that's the system that we will be hitting when visiting a web app for the first time. Bear in mind that it will not, prevent the consumers of a web app from downloading the assets (that's the browser's work), but it will make the load time much faster by placing the static assets closer to the user's device, at least, once they are cached.

Enabling cache for CDNs is usually well-defined within their docs, but the most common practice is setting the `Cache-Control` header to `public` and `max-age` with a value greater than 0, like so:

```
Cache-Control: max-age=31536000,immutable
```
The best/simpler practice in this regard would be to add the `Cache-Control` header to every single asset BUT the ones that are NOT created by the bundler, meaning: `.html`, `.png`, `.gif`... And that's just because they don't pass by a bundling process that allows us to change their name depending on the content.


### CDN Cache Invalidation

Invalidating the cache is a very sensible process and as such, it should be as straightforward as possible, so that we can minimize errors, such as delivering outdated assets.

The simplest and most effective strategy for this is to invalidate the cache every time we make a make a new deployment, for all the assets that are not part of our bundling process, such as:

- .html
- .png
- .jpg
- .gif
- ...

And this is simply because even if these assets change, they can still keep the same name and we don't really have a way to signalize the CDN if they are outdated or not.

### Cache Busting

![Cache Busting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kb7ajl88t4ymr21kzc3.png)

> _Cache busting is a technique used by web developers to force the browser to load the most recent version of a file, rather than a previously cached version._ - [keyCDN](https://www.keycdn.com/support/what-is-cache-busting)

We still need to somehow signalize both the CDN and the Browser if we should get a new asset or re-use the cached one. And the best way to do that is through `cache busting`.

Cache busting can be applied with different strategies, such as:

```js
// File name versioning
"index.v2.js"

// File path versioning
"/v2/index.js"

// Query params versioning
"index.js?v=2"

// File content hashing
"[name].[contenthash].js"
``` 

### File content hashing

From my experience, **content hashing** is the best `cache busting` strategy for 3 reasons:

1- we don't rely on past data (like in versioning)
2- we can still use cached assets even after new deployments
3- cache busting is done automatically by having differently named files when their content changes

In Webpack, to generate the chunk names using their content hashes, we  can change the output format like so:

```js
// webpack config
module.exports = {
  entry: {
    app: './src/app.js',
  },
  output: {
    // entry bundle chunks names
    filename: '[name].[contenthash].js', // 🆕
    // non-initial chunk names
    chunkFilename: '[name].[contenthash].bundle.js', // 🆕
    path: path.resolve(__dirname, 'dist/app'),
    // removes old bundles from the directory
    clean: true, // 🆕
  },
  ...
};
```
Once we build our web app with such configuration, we will have something similar to this:

```
📁 dist/
 ├── 📄 index.html
 ├── 📄 app.575d644de9307ca8621d.js
 └── 📄 vendor-react.7c8b137d61bff5cad6e7.bundle.js
```

## Summary

1- Extract common parts of code into their own chunks;
2- Distribute your web app through a CDN;
3- Invalidate non-bundled assets on deployment;
4- Setup a caching strategy through cache busting;

## Sources

Make sure to take a look at these sources if you wanna have a more in-depth understanding of caching and code splitting:

- [Reduce your react bundle size with webpack code splitting](https://www.codemzy.com/blog/react-bundle-size-webpack-code-splitting)
- [Cache busting a React app for production](https://www.codemzy.com/blog/cache-busting-react)

## Conclusion

Distributing cached assets is the best way to spare the clients from downloading unnecessary code and having a much faster loading time. 

One of the best ways to do it is by distributing your web app through a CDN and setting up a strategy that allows for invalidating new assets when their content changes.

---

_Make sure to follow me on [twitter](https://twitter.com/P_FFigueiredo) if you want to read about TypeScript best practices or just web development in general!_