Containerizing JavaScript Applications with Bazel


Containerizing JavaScript applications is controversial because they come in so many flavors. They could be bundled into a single file or the original layout of the source tree could be kept intact. There is not a one-size-fits-all approach to creating a container out of your JavaScript application.
With Bazel, this story is different. All *_binary targets have a well known directory structure called Runfiles which makes it insanely easy to decide what structure the Javascript container will have. You just take the Runfiles directory tree, put it into a tar archive, add it to your oci_image
and call it a day, right?
Though this is a fine approach for small applications (<50MB), it does not scale well beyond a few gigabytes because of increased build and deploy times. You could take a nap waiting for the whole layer to be uploaded and redeployed after a single line change.
The JsImageLayer
rule from rules_js keeps you moving. It is a packaging rule that efficiently creates JavaScript containers using the Runfiles structure.
In the early days, JsImageLayer
created two layers for the whole container. The node_modules
layer contained everything that changed infrequently such as npm dependencies and node interpreter (yes, rules_js
includes a hermetic Node.js interpreter) and the app layer contained first party JavaScript code.
This worked fairly well because a single line change did not cause node
and node_modules
to be uploaded and redeployed. However, we realized that it was not good enough. node binary rarely changes and node_modules
changes more frequently than node
, so it is not economical to bundle them together as a single layer.
That’s exactly what prompted the rules_js
maintainers (us) to add more layers, ordered from infrequently changed to frequently changed.
In version 2.0 of rules_js
, js_image_layer
created more layers for better build and deploy time performance. It worked well for most JavaScript containers, but there is no one-size-fits-all approach. People reached the limit of that optimization too.
$$\[ \begin{array}{ccccc} \text{node} & \text{package_store_3p} & \text{package_store_1p} & \text{node_modules} & \text{app} \\ \uparrow & \uparrow & \uparrow & \uparrow & \uparrow \\ \text{interpreter} & \text{3rd party npm} & \text{1st party npm} & \text{symlinks} & \text{application code} \\ \end{array} \]$$
What happens if you have 150,000 files from 3rd party npm packages? Changing one npm package led to the whole layer being rebuilt and sent over the network, causing unwelcome flashbacks to the early days of js_image_layer
. The problem was even worse if you had npm packages that shipped with prebuilt binaries or .node bindings. In my consulting work with AI companies, I learned how monstrous pip
packages can be (👀 CUDA). I knew what I had to do.
Introducing JsImageLayer
layer groups, a new rules_js
feature that allows fine grained control over the number of layers created. Users can create additional layers to further optimize JsImageLayer
by supplying a dictionary of names and regex that is evaluated against the path.
An example of putting @huge/pkg
into its own layer can be written as follows.
$$\[ \begin{array}{cc} \text{layer_groups} & \text{default layers} \\ \uparrow & \uparrow \\ \text{any number of additional layers} & \text{the layers shown above} \\ \end{array} \]$$
JsImageLayer
creates 5 default layers for an easy out-of-the-box experience even if they are empty due to preceding layer_groups.We also took this as an opportunity to optimize how we generate layers. Previously, js_image_layer
had a custom Node.js program to create layers (.tar
archives). Though it worked great for medium-size archives (<= 200MB), streaming backpressure greatly reduced its efficiency. Fixing this was as easy as building the archives with good ol’ libarchive (also known as bsdtar).
One of our customers saw a 40% speed improvement with no additional configuration change. With some additional layers, it became 50% faster due to better parallelization of build actions.
A benchmark with the cold build times for a js_image_layer
target with no change to the BUILD file demonstrates 52% speed improvement for overall build time.
You can now add as many layers as you want based on size, how frequently they change, or any other criteria. You can even override the default layers by using the same keys in the dictionary.
The Layer Groups feature is now available in rules_js
version v2.3.5!
Subscribe to my newsletter
Read articles from Şahin Yort directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
