Faster Frontend Builds

Things I changed to make local and CI builds faster.

Tagged with: azure devops, docker, javascript

At work, we are developing a single-page application with React. It used the Create React App build tools that, albeit they were not the fastest, did their job. I was bothered by this as it often slowed me down when developing locally but I also had some issues with our build pipeline. Just imagine that you write a fix for a problem and you have to wait about ten minutes for your pull request build and after that, you have to wait the same amount of time for the release build. For an impatient millennial this is unacceptable. I had to change something.

It is common knowledge that in the JavaScript world every problem can be solved by the inclusion of yet another package. So I did exactly that. Instead of using Create React App I updated the project to use …drumroll please… Vite. Locally this made things fast, very fast. Starting the local development server now takes about three seconds instead of thirty. This was already a huge win for us. On the build server, this change did not provide any noticeable speed improvements. That is because, at the moment of writing, Vite uses different mechanisms for development and optimized production builds.

After I was satisfied with the improvements to development builds I started to look at how the Azure DevOps build pipeline that I wrote half a year ago could be improved. This pipeline runs for each pull request and the release build. Its output is a Docker image that includes a web server that serves the built single-page application. Inside the pipeline, a multi-stage Docker build was run. This build used a Dockerfile which first built the application and ran tests inside of a Node.js base image and then copied the built application from the first stage to the final stage that was based on the image of a web server. The major reason for building and testing the application during the Docker build was to ensure that the build is independent of dependencies installed on the build server. This can be a valid reason but in our case, all steps of the build run on a hosted agent which is a container that gets created for the build job and is destroyed after the job finishes. So we do not have to worry about someone installing updates on the shared build server and with that breaking all the builds that relied on a previous version of some dependency. For the frontend build, I decided it was perfectly safe to drop the build inside of Docker, build everything on the agent itself, and as a final step create a Docker image with the finished application. This change alone chopped off one and a half minutes of build time.

Finally, I could rest and enjoy the fruits of my labor, or could I? There was still one thing that bothered me. At the beginning of each automated build, about thirty seconds were spent downloading NPM packages. These packages seldom change and would therefore be a perfect candidate for some sort of caching. This is where the Cache task comes in. This simple task can be used to cache dependencies for a given cache key which can also be the contents of a file. By using the package.lock.json file as a cache key all NPM packages are cached until the file changes. By applying this final improvement most of the builds only spend a tiny amount of time restoring the package cache instead of downloading the whole internet over and over again.

It's over it's done. I could now finally move my attention to the features I should have been building the whole time, but now I can develop them without having to wait for eons (aka seconds) until my development environment spins up and my code is released to production. Unless by coincidence some new JavaScript engine named like a baked good comes along that makes everything run faster I am finished with performance improvements of my build.