Reducing our React bundle size by 50%

How we achieved Code splitting in React with SSR

Bhavin Agarwal
SAFE Engineering
5 min readAug 16, 2021

--

Background

“Why this page renders slow 😭?”,

“Trimmed CSS but nothing works, it still makes no difference”,

“It compressed but still a LOT to deal with 🥺”

Large bundle size is always a problem. You might have found yourself dealing with the above situations where, adding optimizations might give small gains (sometimes huge) but still, you always wish for something more.

Things were fine initially when we used to have small presentational components with less business logic but it became dreadful (actually out of control) when more requirements came in. With every new use case, we thought of using a pre-cooked library and ended up shaking our JS budget 😕

Asking your client (browser or a person 😁) to parse your JS code always takes a toll. What is more frustrating is when these guys do it unnecessarily 😟

Pissed-off man is curious of the ask…

A territory in itself to look upon! And the best weapon we had in our hands was Code Splitting!

This article walks through how we implemented code splitting in React which has its pages being rendered server-side and some challenges we faced alongside 🚶‍♂️

Where did the problem start?

When we initially started with adding React components, we were benefitted from SSR and then hydration which was done at the client-side but still, our APM kept complaining of slower page load times and specifically our bundle size 😕.

One problem that was evident for us was the addition of large npm packages without much earlier thought. Though it was tough for us to replace all heavy npm packages (say moment 😔) due to their roots being tucked deep, we still managed to clean up few packages, SVG files, CSS, etc to make our bundle lighter and happy 😊

The bundle seems fine 🤷‍♂️

But…..

The browser still had to process a lot of Javascript in order to make our pages functional. Can’t we just give it what it needs 🤔 ?

This is where we found and learned about Code Splitting in React! It was all present upfront but how to use it with SSR as there was no native support available 😭

It was this time when we read about loadable-components and it technically changed our lives 🥲. Let’s catch up more below….

How we achieved Code Splitting?

We use Razzle to achieve server-side rendering in our React app. It does not support code-splitting natively and thus, we had to make some configuration changes in how Razzle was setup initially 😐

Webpack in itself provides amazing config options to support code-splitting as an optimization technique. Flexibility available in Razzle made it even easier to explore those! We examined the following pointers to proceed:

  1. Webpack modifications: We made some config changes in razzle.config.js (a file provided by razzle to override webpack config) to split our bundle as per the dynamic imports.
  2. loadable-components: We used loadable-components to help us add important tags from the server-side that hint the browser on what to load and how to load a particular js chunk!

Setting up

  • loadable-components requires a babel plugin and a webpack plugin to resolve our dynamic imports.
yarn add @loadable/server && yarn add --dev @loadable/babel-plugin @loadable/webpack-plugin
  • Once added, we need to update our babel configurations to include the plugin. In our project, we used .babelrc file to define the plugins:
  • Razzle comes with a razzle.config.js , a configuration file that allows us to override the default webpack config used internally. We can use this to configure the loadable webpack plugin.

Note: The above config adds chunks to the build folder. Replace the folder name at line 17 with something else if you serve your production bundle from a different folder ( say dist).

You can read more about other chunk splitting configs offered by webpack here

  • We now need to set up the Chunk Extractor in server.tsx which actually creates chunks as per the dynamic imports and emitspreload/prefetch link tags (we would learn a bit more about it at the end 😉).
  • The above snippet provides a server-side rendered document with chunks being loaded asynchronously. But we need to hydrate our app once they are ready. For this, we need to wrap our app in client.tsx file under loadableReady() function
  • Finally, it was time to use loadable components in our App! Though there are no hard rules on where to apply code splitting. We chose our routes inside App.tsx as candidates for splitting code. For this, we exported all the Async components from a common file and used those components in App.tsx .

🎉! Once the setup was done, let us see how it finally impacted our App bundle 🚀

Before and After Code Splitting

Here are some surprising stats:

Before splitting:

It was a huge chunk of ~1.4MB which included all the dependencies even if was not used.

Single large bundle loaded on every page

After splitting:

The largest bundle came down to just ~384kB and a total bundle size, which was loaded on the first page, to ~795kB

The bundle was split into multiple JS chunks!

That was about ~43% improvement on the JS that was being served on our main dashboard! Pretty huge right 😯?

Other improvements over code splitting

While implementing code-splitting, we came across the concept of preloading and prefetching. In a nutshell, preloading loads chunks in parallel which is the default behavior, while prefetch chunks are loaded only after the parent chunk are finished loading and the browser is idle. The prefetched chunks are kept in HTTP cache and loaded from there once demanded.

Given this info, we put some of our React components that were not interacted with by the user immediately after a page visit like Modals , Notification etc. This was achieved using webpack’s magic comments:

We created a list of these AsyncComponents over the reusable base components to be used as per the needs. Really helpful 😃!

Below is how a browser prefetches a chunk in idle time and loads it from the cache when demanded:

You can read more about the same in this excellent article.

Conclusion

Though, we should not put everything to be lazily loaded as that might lead to a bad UX since the user has to wait for every lazy-loaded UI element to be loaded as clicked/interacted with. Some initial config and changing the static imports to dynamic ones help up maintain our JS budget a lot.

A great way to serve JS to the browser which is just needed :)

Happy coding 🚀!

--

--