How I reduced bundling time of a React app from 10 minutes to 1 minute
This article is for newbies who are not so much familiar with the build setup with Webpack, or someone who was pulled into an old project that needed some expert hands.
Recently, I had the opportunity to look into a React (v16) project which had been in development for the past 4 years. It was a client-side-rendered app, wired up from the scratch with the help of Webpack 4 (no frameworks) and was quite a large application, given the list of functionalities it offered.
But, the very first thing that struck my curiosity was the time that it took to boot up the dev server, which was more than 10 minutes. The Prod build was worse, and even the HMR took nearly 2 minutes for some files. In my experience, that number was on a cosmic level even for a larger project than this one. Having thought about the lag during development time, I instantly decided to do something about this.
I’m not a person who normally hates things at first sight. It takes quite a while for me to hate things. But I got to say this — I hate a setup that takes 10 minutes to start up a dev server, and 2 minutes for HMR to kick off the changes.
Server startup time was not just the only problem I observed. I took a prod build, and found that it weighed more than 1.7Mb gzipped. Well, quite too obese for an application of its scale. My first instinct was to go through the dependencies to see what it was eating up. And yeah, I was not really wrong about that. It used some heavy packages like ace-builds + its react counterpart, carbon-components and its react versions, usage of both Lodash and Underscore and a lot of other stuff that did not make any sense to me. Also, there were a handful of build-time dependencies included as runtime dependencies in the package json.
But — there was a catch. Webpack.
Somebody had done a good job configuring Webpack when the project initially kicked off. Although outdated (v4), Webpack was doing its thing without much mistakes. So, all those build-time dependencies never really affected the bundle, and those bulky libraries were not that much of a headache as it seemed in the first glance.
But I still doubted the outdated version of Webpack, and hence thought of giving ‘Vite’ a try, to see if it would do some magic and make me an instant winner. Unfortunately, what happened was just the inverse. I instantly regretted my choice once I completed setting up Vite and ran the build. Not only did it take more time to get it bundled, but it also made the bundle bulkier than it was. I can’t complain ‘Vite’ for this, as it is primarily a native ESM bundler (which is the main reason behind its blazing speed). But since the project was kind of old and had outdated dependencies which had a lot of CommonJS implementations, I believe ‘Vite’ was not able to do its trick. Hence, I went back to the good old Webpack.
A thought crossed my mind to migrate the whole setup to Webpack 5, and I discarded it soon, because it would totally slip out of the limited timeframe that I had in hand. I was under no liberty to mess around all those dependencies and its implementations in the application since it was a self-sponsored task after all. Hence I decided to stick with Webpack 4 and play within my constraints.
What other reasons might there be?
Webpack is like a black box. It is difficult to understand what is happening under the hood unless you enable a few things. Thus, my first step was to add a progress plugin to Webpack and a bundle analyzer. Webpack did have a progress plugin OOTB, but I went with the Simple-webpack-progress plugin which was quite easy to use and looked elegant. It had a few logging styles, of which I went with ‘verbose’ for the sake of squeezing the most out of the build process.
The existing configuration was set up to open the browser when the build process started. But to me, this was a bit deceiving. So I moved it into the ‘done’ hook of the compiler, so as to open the browser only upon build completion. The very first startup after this setup itself gave me the biggest clue about the villain — the build was evidently lagging/stuck when it started compiling several stylesheets.
A brief about the stylesheets in the application — it was using SASS, and CSS modules (maybe I should call it SASS modules?). It was loaded using sass-loader, css-loader, postcss-loader and style-loader in the dev config, and style-loader was replaced by mini-css-extract-plugin in the Prod config for obvious reasons.
So naturally, I began to suspect the stylesheets. I thought they were enormous. So I opened one of the files that was ‘stuck’ in the build, and to my surprise, found that it was just some 30-ish lines long! I opened several others, and it was not much different. They were normal stylesheets, with only a decent number of lines. Puzzled, my suspicion diverted to the outdated loaders. I had valid reasons to believe so. The css-loader that was being used was 1.0.0, whereas the current version is 6.7.4 (at the time of writing this)! The case was not any different with the other related loaders. Each one of them had seen their good days. Hence, went on updating all the loaders that were collectively involved in loading the stylesheets. I couldn’t update them to the latest versions, since the latest versions demanded Webpack 5, but I was able to update them to their latest versions that were meant to work with Webpack 4. It took time, as the documentation was not easily available for the intermediate versions and several times, it broke the build.
After changing config back and forth, I managed to get a successful build, only to see that nothing changed much. The dev build time got cut down by 2 minutes, making it 8 minutes for dev server startup. But the suspected stylesheets kept the build slowing down without any changes. And, that’s when, I thought of giving up. But somehow, I didn’t.
“Whenever you think of quitting, take a nice nap” — said a wise man once. So the next day, I started fresh, and this time, I started giving those ‘innocent looking’ stylesheets a generous peek. I was right when I said most of them were quite lean. But what if that log was wrong? What if the file being shown as stuck is just the file before the actual culprit file? This thought led me to analysing all the lagging stylesheets. And guess what I found? A single line that imported the whole of the carbon design system, repeated in at least in 12 files.
Carbon design system is no joke. It is a full blown library that includes styles for around 40+ components, themes, layouts and what not. And each of our builds is actually compiling this gigantic library, almost 12 times. What else do you want to burn your laptop?!
Now with the mystery unraveled, the fix was pretty straight forward. Why do we import a SASS file? Most of the time, it is just for using a variable, or including a mixin. For the same reason, Carbon components allow importing only required partials without having to import the whole library. I went on to replace the full import with the corresponding partials. The core stylesheet had to be included in the root stylesheet though, since the whole application needed the styles from the design system. Ran the build again, and guess what, the build time went down from 8 mins to 2 !! The Prod bundle also saw a 500Kb reduction in its size as well, which came as a bonus to me.
The Final touch
Although I managed to reduce the build time to 2 minutes, the sight of root stylesheet being ‘stuck’ for a while in the logs was not giving me a whole-hearted winning laugh. That’s where the cache-loader kicks in. ‘cache-loader’, as the name suggests, caches the output of the loaders specified after it, in the loader chain, and re-compiles them only when there is a change. Now, the first build — the very first build after ‘cache-loader’ is implemented — takes approximately 2 mins. From then on, it just takes no more than a minute. Voila!