ClojureScript optimizations on Node - huh?

A key design decision early on in the development of the ClojureScript compiler was to leverage Google's Closure compiler for optimizing the generated JavaScript.

The Closure compiler is able to optimize both the size and efficiency of a given JavaScript project by doing whole-program analysis of the source code. The analysis allows the compiler to remove whitespace, inline code, rename sumbols, and most importantly eliminate dead code from the project thus reducing code size dramatically.

Optimization levels

The ClojureScript compiler can run in four different optimization levels:

  • none
  • whitespace
  • simple
  • advanced

None

The :none level, as the name implies, turns the optimizations off, ie the Closure compiler is not run on the JavaScript emitted by the ClojureScript compiler.

Whitespace

With the :whitespace optimization level the Closure compiler will be executed, and it will remove any unnecessary whitespace and punctuation (eg extra parentheses) from the JavaScript.

Simple

The :simple level enables optimizations within function bodies. In additions to removing whitespace and extra punctuation, the Closure compiler will also rename symbols that are local to functions. Since JavaScript is stored as source code, renaming symbols can result in significant space savings.

Advanced

The highest optimization level is :advanced. The :advanced level turns on all the optimizations allowing the Closure compiler to rearrange and inline code, and eliminate unreachable code. This results in dramatic space savings, but if the source code does not conform to all the assumptions made by the Closure compiler, the resulting code will not run correctly. For example, if the source code references libraries that have not been compiled with the Closure compiler, the renaming of symbols performed by the Closure compiler will break the references.

Setting an optimization level in your project

I'll assume here that you're using lein-cljsbuild to run the ClojureScript compiler. In that case the :compiler map in your project.clj file should have an :optimizations key and you can change the level of optimization applied to your code by setting the key to :none, :whitespace, :simple, or :advanced:

:compiler {:output-to "test.js"
           :output-dir "out"
           :target :nodejs
           :optimizations :simple} ;; :none, :whitespace, or :advanced

The lesser optimization levels are useful during development and testing since the emitted JavaScript code and any stack traces are more readable. Production code should probably be compiled with the :simple or :advanced levels in order to reduce code size and increase performance.

If you're not using lein-cljsbuild, you can checkout the Node.js section of the ClojureScript documentation to see how the optimization level can be set.

Optimizations levels on Node

In the past ClojureScript on Node would support only :simple and :advanced optimization levels. The reason was that the other two levels do not inline the generated JavaScript into a single file. This makes it hard to load the code in Node, because Node doesn't provide a way to simply load a JavaScript file into the runtime. Instead the code should use Node's require() function to import code.

Fortunately David Nolen added support for the missing optimizations levels just the other week. The change isn't in the latest ClojureScript release (0.0-2173) yet, but it should be included in the next release (anything after 24 Feb 2014).

Using the new optimization levels

You can follow along by running lein new nodecljs test to generate a test project, and setting up lein-cljsbuild to use a git checkout of ClojureScript.

The bad

If you change the optimization level to :none and try to run your project, you'll be greeted with something like this:

% node test.js

.../test/test.js:1
(function (exports, require, module, __filename, __dirname) { goog.addDependen
                                                              ^
ReferenceError: goog is not defined
    at Object.<anonymous> (.../test/test.js:1:63)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:902:3

Since the :none level (and the :whitespace level) doesn't inline the JavaScript code into one JS file Node doesn't know to load the Google Closure standard library.

The ugly

To get all the required files loaded you have to jump through some hoops:

% node
> require('./out/goog/bootstrap/nodejs')
{}
> require('./test')
{}
> require('./out/test/core')
{}
> test.core._main()
Hello world!
null

First we teach Node how to dynamically load Google Closure generated code. Then we load our 'main' JavaScript file, which maps Closure namespaces to JavaScript files. The third require is used to actually load our ClojureScript code, in this case the test.core ClojureScript namespace. Finally, we execute the (test.core/-main) function.

Notice that the ClojureScript name has been mangled into a JavaScript compatible name by the ClojureScript compiler.

The good

Jumping through all these hoops just to get your code running seems like a massive annoyance. Fortunately it's all just JavaScript, so it's simple enough to automate it all with a little script (put this in eg main.js):

require('./out/goog/bootstrap/nodejs')
require('./test')
require('./out/test/core')
test.core._main()

And run main.js instead of test.js:

% node main.js
Hello world!

Not so bad after all.

What's the point of it all?

So why would you bother with the two new optimization levels if it's so inconvenient to set up? Two orders of magnitude speed increase, that's why!

With :simple:

Compiling "test.js" from ["src"]...
Successfully compiled "test.js" in 2.145 seconds.

With :none:

Compiling "test.js" from ["src"]...
Successfully compiled "test.js" in 0.028 seconds.

I've complained in the past that enabling source-map support in a ClojureScript project doubles the compilation time. With a hundredfold speed increase as a baseline, a twofold speed decrease doesn't sting so much anymore.

Conclusion

It's nice to see that ClojureScript on Node is slowly gaining parity with ClojureScript on the browser. Ideally the two platforms would behave identically, but that might not be possible in all cases, because of the fundamental differences in the two platforms. Nevertheless, supporting all four optimization level on both platforms is definitely a step in the right direction.

The compilation speed increases resulting from not calling into the Google Closure compiler are very impressive. Anything that shortens the feedback loop between code changes and seeing those code changes execute is a good thing. Hopefully we can work out a way to avoid having to manually write the bootstrapping JavaScript file in the near future.


Want to read more about ClojureScript on Node?
Sign up for our mailing list!

Unsubscribe at any time. No spam, ever.
comments powered by Disqus