> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Containerizing JavaScript Applications with Bazel

> Learn how to optimize JavaScript container builds with rules_js's JsImageLayer. Discover layer groups for better build times and deployment efficiency.

export const BlogPost = ({title, date, authors, tags, image, children}) => {
  const tagList = tags ? tags.split(", ").filter(Boolean) : [];
  const tagSlug = t => t.toLowerCase().replace(/&/g, "").replace(/\+/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  const formattedDate = date ? new Date(date + "T00:00:00").toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric"
  }) : "";
  return <section className="w-full flex justify-center px-4 py-12 md:py-16">
      <div style={{
    maxWidth: "800px",
    width: "100%"
  }}>
        {image && (typeof image === "string" ? <img noZoom src={image} alt={title} className="w-full rounded-xl mb-8" style={{
    maxHeight: "400px",
    objectFit: "cover"
  }} /> : <div className="blog-post-hero-image">{image}</div>)}
        <h1 className="text-3xl md:text-4xl font-bold text-zinc-900 dark:text-white">
          {title}
        </h1>
        <div className="flex flex-wrap items-center gap-3 mt-4 text-sm text-zinc-500 dark:text-zinc-400">
          {authors && <span>{authors}</span>}
          {authors && formattedDate && <span>·</span>}
          {formattedDate && <span>{formattedDate}</span>}
        </div>
        {tagList.length > 0 && <div className="flex flex-wrap gap-2 mt-3">
            {tagList.map(tag => <a key={tag} href={"/blog/tags/" + tagSlug(tag)} className="px-2 py-0.5 rounded-full text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-700 dark:hover:text-blue-300 transition">
                {tag}
              </a>)}
          </div>}
        <hr className="my-8 border-zinc-200 dark:border-zinc-700" />
        <div className="prose dark:prose-invert max-w-none">{children}</div>
      </div>
    </section>;
};

export const MarketingPage = () => <div className="marketing-page-marker" style={{
  display: "none"
}} />;

export const Section = ({children, className = "", gray = false, dark = false, id}) => <section id={id} className={`w-full flex justify-center px-4 py-16 md:py-24 ${gray ? "bg-gray-50 dark:bg-zinc-900" : dark ? "bg-zinc-900 dark:bg-zinc-950" : ""} ${className}`}>
    <div className="w-full" style={{
  maxWidth: "1140px"
}}>
      {children}
    </div>
  </section>;

<MarketingPage />

<BlogPost title="Containerizing JavaScript Applications with Bazel" date="2025-04-21" authors="Şahin Yort" tags="Containers & OCI, JavaScript, Bazel">
  <img noZoom src="https://mintcdn.com/aspectbuild/x1L7Iep716jCyJVo/images/blog/hashnode/hn-7740d73e29.jpeg?fit=max&auto=format&n=x1L7Iep716jCyJVo&q=85&s=9d9c952fa795dc8b9abe4833c006be4b" alt="" className="blog-post-cover" width="1600" height="1064" data-path="images/blog/hashnode/hn-7740d73e29.jpeg" />

  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 tar](https://bazel.build/extending/rules#executable_rules_and_test_rules)[get](https://en.wikipedia.org/wiki/Tar_/\(computing/\))[s](https://bazel.build/extending/rules#executable_rules_and_test_rules) 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](https://en.wikipedia.org/wiki/Tar_/\(computing/\)) 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.

  <div data-node-type="callout">
    <div data-node-type="callout-emoji">💡</div>
    <div data-node-type="callout-text">If you don’t know about how the default Docker storage OverlayFS works, give <a target="_self" rel="noopener noreferrer nofollow" href="https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs/" style="pointer-events: none">this fun page</a> a try.</div>
  </div>

  The [`JsImageLayer` rule from r](https://github.com/aspect-build/rules_js/blob/main/docs/js_image_layer.md)[ules\_js keeps y](https://github.com/aspect-build/rules_js/tree/main)ou 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](https://www.aspect.build/services) 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.

  <img noZoom src="https://mintcdn.com/aspectbuild/x1L7Iep716jCyJVo/images/blog/hashnode/hn-8d7fc080df.png?fit=max&auto=format&n=x1L7Iep716jCyJVo&q=85&s=9af2f18bbe05bf5b02c1efeaa8eb8d5b" alt="" className="block mx-auto" width="2058" height="865" data-path="images/blog/hashnode/hn-8d7fc080df.png" />

  $\[ \begin{array}{cc} \text{layer_groups} & \text{default layers} \\ \uparrow & \uparrow \\ \text{any number of additional layers} & \text{the layers shown above} \\ \end{array} \]$

  <div data-node-type="callout">
    <div data-node-type="callout-emoji">💡</div>
    <div data-node-type="callout-text"><code>JsImageLayer</code> creates 5 default layers for an easy out-of-the-box experience even if they are empty due to preceding layer\_groups.</div>
  </div>

  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](https://nodejs.org/en/learn/modules/backpressuring-in-streams) 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](https://github.com/thesayyn/js_image_layer_bench) 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.

  <img noZoom src="https://mintcdn.com/aspectbuild/x1L7Iep716jCyJVo/images/blog/hashnode/hn-ff52dc892a.png?fit=max&auto=format&n=x1L7Iep716jCyJVo&q=85&s=204793af8ae0513d119b76e19b49dc69" alt="" className="block mx-auto" width="2572" height="1598" data-path="images/blog/hashnode/hn-ff52dc892a.png" />

  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](https://github.com/aspect-build/rules_js/blob/main/docs/js_image_layer.md#js_image_layer-layer_groups) is now available in `rules_js` version [v2.3.5](https://github.com/aspect-build/rules_js/releases/tag/v2.3.5)!
</BlogPost>
