In this post I will show how to build a scalable React dev environment using Bazel as the build tool.
I previously wrote an article on how to use Bazel to build a fullstack React + Java project.
The focus of my previous article was to show how to use Bazel as the single build tool in a mixed programming language environment. A key benefit of this is the ability to share typing information across language boundaries. In this case between a React frontend and a Java backend.
In this article the focus is more on showing how Bazel scales as a build tool even as the codebase grows very large.
The sample project used for this article is the same project I used for the previous article. Except I have exaggerated the size of the React application by growing it to more than 8000 components.
In short, I am building > 8000 React components and a Java backend using Bazel as the only build tool.
Build Performance
As your project grows, you may have noticed a gradual slowdown in build times. With some build tools you see a linear slowdown of build times as the number of components grows. At a certain point this may impact developer productivity.
In this project I will show how Bazel addresses this scalability problem.
The key to scalability is incremental builds. Limiting rebuilds to only code that changed is orders of magnitude faster than rebuilding the entire application.
In Bazel you can define compilation units as granular as a single React component. This means the granularity of a re-build can be as small as a single component. If you only changed a single component, only that component, and code depending on the component will be rebuilt.
Demo Project
You can download the demo project from Github. Make sure to check out the “large-app” branch.
Follow the instructions in the readme to launch the application.
How do you define the compilation units?
In Bazel you have to define a set a Bazel rules. Rules are kind of analogous to Webpack rules. Think of the rules as a set of instructions that tells Bazel how to build the application.
Bazel offers rules for pretty much any build operation imaginable. Here we need something that can transpile Typescript (tsx) to Javascript. It turns out the ts_library rule will do just that. By defining a ts_library Bazel rule per component, I achieve the per component granularity of the build.
Just place a BUILD.bazel file next to each component file with a reference to the component .tsx file as seen below:
Deps declares dependencies on rules in other BUILD.bazel files.
In this case the component depends on another ts_library instance called //src/model. The full build graph is made up of a tree of ts_library instances.
First Build
Incremental builds make Bazel scale, but what happens when you’re building the project for the first time?
During the initial build, nothing is “already” built, so a full rebuild is required.
You may have noticed that my project is relatively slow to build the first time (~10 min on my machine). That said, keep in mind the size of the project, but also the fact that Bazel is also building and launching a full Java api server.
Once the application is done building, try making a code change to one of the components. This is where the incremental build approach really shines. You will hopefully see rebuild times around 1-2 seconds.
Next, try restarting the build. You will see that the second build is much faster than the first build. This is because Bazel persists the build artifacts to disk, and reuses them the second time around. Think of this as super efficient build artifact caching.
In this example I am also making use of a remote cache for the build artifacts. The first time you build, watch for a series of PUT requests in the terminal. This just means the cache is being built during the first build.
Once the build completes, the next build will use local build artifacts. However, if you run bazel clean to clear out the local build, subsequent builds will pull artifacts from the remote cache. Watch for GET requests in the console.
In a more realistic setup, the cache would run on a different server. Here I am just hosting it in a docker container using nginx as the cache server.
Still, what can we do about the slow first build?
It turns out that Bazel supports remote builders as well. Imaging 100s of remote builders running in the cloud. Here is a video with a demo of what this might look like. This demo shows an example with 600! cloud builders building an application in parallel. Basically this is the same as building your application on a computer with 600 cores.
Imagine how that would improve build performance in my app...
I should note that some of this is a bit experimental and still in the POC stage. I am also exploring how to set up remote builders in my project. Hopefully I will be able to share progress in a future post.