Closure Compiler in Planck

August 17, 2017

Planck is now built using Closure Compiler optimizations. In addition, it is now trivial to apply Closure Compiler to scripts executed by Planck in order to optimize them.




To date, Planck (as well as Replete) have been using :none for compiler optimizations. This is ideal for a REPL because it

  • avoids symbol renaming
  • avoids DCE (so the entire runtime library remains available)

But, these properties also hold for the whitespace only and simple Closure Compiler optimization modes. (They only fail for advanced.) Because of this, self-hosted ClojureScript supports up to :simple.

Planck 2.7.0 and Replete 1.12 now ship with Closure Compiler optimizations applied ahead-of-time to their bundled namespaces. You can readily see this if you (set! *print-fn-bodies* true) and print some of the functions in cljs.core.

Closure Compiler yields JavaScript code that is amenable to compression: In Replete's case, using simple and gzip reduces the size of the installed app from 27 MB down to 7 MB. I'm glad Replete can do its part to help fight mobile app bloat!

Using simple also makes some things in the bundled ClojureScript standard library run a bit faster: In Planck, (reduce + (range 1e7)) now completes in about 0.10 s on my machine, whereas previously it would take roughly 0.19 s.

The way Planck internally uses simple is not to concatenate all of the bundled scripts into a single monolithic script to be loaded at startup time. Instead, compiled scripts are maintained separately, per namespace, and lazily loaded. This helps keep startup latency low. With simple

time planck -e 1

now completes in about 0.19 s on my machine, whereas previously it would take 0.23 s (17% faster).

Optimizing Your Scripts with Closure Compiler

Planck exposes the capability to apply Closure Compiler optimizations to Planck scripts. A new command-line option -O / --optimizations is introduced which can take values of none, whitespace, or simple.

If you set this option to anything other than none (the default), then any ClojureScript code loaded from a namespace during Planck's execution will additionally have Closure Compiler applied to it using the specified level.

This is possible because the JavaScript version of Closure Compiler is now bundled with Planck. Clojure Compiler in JavaScript was announced about a year ago. And about half a year ago, the ability to this new facility in self-hosted ClojureScript for code generation was introduced with Lumo. Credit to António Monteiro for blazing this trail!

Since applying Closure Compiler can take a few seconds on some namespaces, when startup latency needs to be minimized, it makes sense to combine this feature with Planck's ability to cache the compiled JavaScript using -k / -​-​cache or -K / --auto-cache. When doing this, as a bonus, you can more easily see the entirety of the optimized JavaScript generated by peeking in your cache directory. If you later switch to a different optimization level, any cached JavaScript will be re-generated.

To illustrate the effects simple can have on the code generated in Planck, consider this function, which illustrates opportunities for variable name shortening and constant folding:

(defn f [long-name]
  (let [process (fn [foo]
                  (str "abc" "def"
                    (+ 1 2 3) foo))]
    (process long-name)))

Here is what you'd normally get for f:

function foo$core$g(long_name) {
  var process = function(foo__$1) {
    return ["abc", "def",
      cljs.core.str.cljs$core$IFn$_invoke$arity$1(1 + 2 + 3),
      cljs.core.str.cljs$core$IFn$_invoke$arity$1(foo__$1)
    ].join("");
  };
  return process.call(null, long_name);
}

With --optimizations simple and --static-fns, this is produced for f:

function(a) { return ["abcdef",
  cljs.core.str.cljs$core$IFn$_invoke$arity$1(6),
  cljs.core.str.cljs$core$IFn$_invoke$arity$1(a)].join("");
}

When Closure Compiler is applied, the JavaScript emitted by the ClojureScript compiler is further transformed, thus changing source code locations. Planck takes care of ensuring that the additional source mapping information is composed so that stack trace locations are correct when using -O / --optimizations with either whitespace or simple.

I hope you enjoy this new capability, either to make your own scripts run faster, or simply as another avenue for learning about what Closure Compiler has to offer for ClojureScript code!

Tags: Planck ClojureScript Replete