Using the Closure Compiler with Bazel

Published: 03/04/2020

In this post I will show how to build a JavaScript application using Bazel and the Closure compiler.

Closure Compiler

The Closure compiler is an advanced tool for optimizing Javascript applications. The idea is that the Closure compiler will compile your JavaScript to better, more optimized JavaScript. Standard JavaScript optimizers are limited to safe techniques like tree shaking and conservative minification, but the Closure compiler will take this a step further and potentially rewrite parts of your application. In my experience Closure compiler bundles will be 30-40% smaller than bundles created by standard JavaScript bundlers.

In the following section I will show an example of how the Closure compiler may rewrite the original application for better performance.

Let’s take a look at a very simple greeting application consisting of the following three files:

greeting-service.js
import { Greeting } from './greeting.js'; export class GreetingService { getGreetings() { let greeting = new Greeting(); return [ greeting.createGreeting('Joe'), greeting.createGreeting('Jane'), greeting.createGreeting('Jim') ]; } }
greeting-component.js
import { GreetingService } from './greeting-service.js'; let greetingService = new GreetingService(); let greetings = greetingService.getGreetings(); for (let i = 0; i < greetings.length; i++) { let div = document.createElement('div'); div.innerText = greetings[i]; div.className = 'badge badge-secondary'; document.body.appendChild(div); }
greeting.js
export class Greeting { createGreeting(name) { return `Hello ${name}`; } }

The application is simple enough, but it consists of several layers and objects. A standard bundler would go through and combine all three files into one file and minify the symbols, but not much more.

Now, let’s take a look at the Closure compiled output below:

var a;a=["Hello Joe","Hello Jane","Hello Jim"]; for(var b=0;b<a.length;b++){var c=document.createElement("div");c.innerText=a[b];c.className="badge badge-secondary";document.body.appendChild(c)};

The most interesting part is the array of greetings. The original code created greetings via a greeting service and a greeting object model, but in the optimized code, all this code is collapsed into a simple array. Basically the Closure compiler analyzed the application and determined that the output will always result in a simple array of strings. This is a perfect example of the types of optimizations the Closure compiler may make to your application.

Closure compiler optimizations are impressive, but this comes at a cost. Since the optimizations are so aggressive, there is a always the chance that you may end up with a broken application if you're not careful. The biggest challenges are usually with third party dependencies that were written without the Closure compiler in mind. However there are techniques (e.g. externs) that can be used to protect incompatible code from dangerous optimizations.

Bazel

Colsure compiler integration with Bazel is supported through the rules in rules_closure.

The two main rules are closure_js_library for creating modules, and closure_js_binary for creating a runnable JavaScript application.

closure_js_library( name = "greetings", srcs = ["greeting-service.js", "greeting-component.js", "greeting.js" ], no_closure_library = True, suppress = ["moduleLoad", "JSC_UNKNOWN_EXPR_TYPE"] ) closure_js_binary( name = "my_app", entry_points = ["src/client/greetings/greeting-component.js"], compilation_level= "ADVANCED", deps = [":greetings"], )

Demo

I have added this sample to Github in case you are interested in trying it out yourself. In addition to the Closure application there is also a Node api in there to serve the application.

Follow me on twitter @MoreTechStories