Knowing this one ClojureScript gotcha will save you hours

September 20, 2015

Summary: ClojureScript optimizes names by replacing them with shorter ones. Usually, that's a good thing. But it can get carried away. Externs are how you help it know what's unsafe to optimize.

Problem

Here's the situation: you're writing ClojureScript code, compiling it with no optimizations (because it's faster for development). Everything is working great. You compile it with advanced compilation and test it, and things start breaking. Hopefully it's just on your local machine and not on production. What's happening?

Analysis

Surprisingly, this happens a lot. As a Clojure programmer, I'm not used to really having a difference between development and production. It's the same language and everything is available in both environments.

But with ClojureScript, it helps to think of it as a compiled language. There's a real difference between development and production. The difference is that the compiler optimizes the code. In advanced compilation (what you want to do in production), variable names are shortened.

Here's some ClojureScript code and the compiled JavaScript (using advanced optimizations and pretty printing) that uses the popular JavaScript library marked for parsing Markdown:

ClojureScript

(defn parse-markdown [s]
  (js/window.marked s))

JavaScript

function Af(a) {
  return window.hc(a);
}

It's nice that the structure of the output is similar to the input. But notice that all of the names in the JavaScript are one or two letters. That's one of the ways a JavaScript file is minified. The compiler makes sure that this all makes sense where it can. And it can for all of the variables and namespaces you define.

But it cannot for variables defined in other JavaScript included in the page. The compiler has no way of knowing by itself that this should not be shortened. Look at the ClojureScript code on the left: it's referring to js/window.marked. And when it's output, it's called window.hc. That's no good and it's no wonder this code doesn't work in the browser--hc is not defined.

Solution

Of course, ClojureScript has a solution for this problem: externs. You can specify a list of files that contain all of the variables that you use that the compiler should not optimize away. Setting up your externs is easy. Just add this to your :compiler options in the approprate build:

:externs ["externs/marked.js"]

I typically create a directory called externs/ right at the root. Be sure to add this to your version control system. Then I make a file, in this case called marked.js, to put all of the variables I'll need to access from that library. Inside:

window.marked = function(){};

It's just a JavaScript file, but I don't need to give it the actual values. This is just for the compiler to know what to look for. See how window.marked is given a do-nothing function? It could be any function, and that's just the shortest way to write one. This tells the compiler two things:

  1. Please don't minify this variable.
  2. You can expect a function there.

If you have to define something that's a number, you would write this:

window.some_number = 1;

You can extend this to any type.

Alternate solution

Because the extern files are just JavaScript, you can normally simply use the JavaScript library as its own externs file. So I'll change the externs entry to point to the file included in the HTML.

:externs ["resources/public/js/marked.min.js"]

This works, but it produces 145 warnings. There is a compiler option called :externs-validation that you can turn off to suppress those warnings.

:closure-warnings {:externs-validation :off}

That also goes in your :compiler options in the cljsbuild build.

Conclusions

The Google Closure Compiler has some very advanced optimization settings. Since they're too slow to run during development, we typically will turn them off while we're coding and testing things out. However, when we turn them on, stuff can stop working. Most of the time it's due to a missing extern. If code that relies on something outside of ClojureScript stops working, that could be why. Check your externs.

If you're interested in getting started with ClojureScript, I recommend LispCast Single Page Applications with ClojureScript and Om. It uses Om to build an application from the ground up. The course teaches everything you need using animations, exercises, code screencasts, and more. It's the fastest and most effective way to learn to build Om applications.

You might also like