How can more layers be more efficient?

Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.

Summary: It's common that adding more layers of abstraction or indirection will make things slower. However, React and ClojureScript make web pages faster than doing it by hand — essentially programming the bare web. The lesson is that if you choose your layers well, they can actually make your system faster.

David Nolen produced an article showing how, out of the box, using React with immutable data structures was faster than using the more common MVC frameworks, for instance Backbone. He demonstrated it using the popular TodoMVC, which is a collection of many, many, many implementations of the same basic app. When implementing the TODO app in a straightforward way, React with ClojureScript beat the others when performing lots of operations.

It's a bit of a perplexing problem. On the one hand, you've got the Backbone implementation that is a very thin layer and is manipulating the DOM directly. It's using mutable state. On the other hand, you've got React with ClojureScript. Everything in ClojureScript is known to be slower than everything in Backbone.

  • Backbone manipulates the DOM elements directly, while React generates a "virtual dom", analyzes it, and modifies the DOM indirectly. React definitely does more work.
  • Backbone was using regular JS arrays and objects. Modification and reading are very fast. ClojureScript, on the other hand, uses sophisticated immutable data structures that are known to be slower for random modification and access.
  • Backbone was using vanilla JS. ClojureScript is compiled to JavaScript and so probably has some overhead compared to hand-written JS.

So it's no wonder it's a little hard to believe. But there's some facts left out of the balance above.

Before we go into those, I should mention that a lot of people argue with the results David Nolen got because he used a very uncommon use case that the other apps were not optimized for. He added a large number of TODO items at once. But David Nolen didn't optimize his for that, either. It only reinforces the point: why is it that when the number of operations gets large, React + CLJS starts to win?

The high level is this: each of the layers that React and ClojureScript add are optimizers. Insert some optimizers in the equation and things start getting faster.

  • The React Virtual DOM is a DOM change optimizer. It turns out JavaScript is fast but the DOM is slow. If you can do some calculations in JavaScript that keep you from writing to the DOM, it's often a win. That's what the Virtual DOM does. It figures out the minimum set of DOM changes needed. And it does it faster than actually making those DOM changes. That's a big win.
  • ClojureScript's immutable data structures are technically slower for each operation, but because they're immutable, you get something you could never have in a mutable data structure. How do you know if the data associated with your View has changed? If it's mutable, you have to walk through the data, all the way down to the leaves, and compare each thing to a copy that you have not changed. But with an immutable data structure? You just have to compare the pointers to your two values. If the pointers are the same, it hasn't changed, and you don't have to revisit that data. Immutable data structures are a change detection optimizer. It's a huge win that can cut out tons of work and repainting.
  • Finally, the ClojureScript compiler itself has an optimization step. Naively compiled ClojureScript is not that bad performance-wise, but when you crank it through the Google Closure compiler, it churns out super-optimized code that screams. I don't think Google Closure contributes much to the TodoMVC example. But it certainly doesn't hurt. So even though ClojureScript is adding on a high-level layer, it's not slower than hand-written JavaScript. It's potentially faster than a clean, hand-written JavaScript implementation, especially as the logic gets more complicated. Because the optimization is done at compile time, you only pay the price once and your users never do.

It's a bit like whether it's faster to fix your toilet yourself or to get a plumber to do it. Sure, there's some overhead of scheduling an appointment, travel, etc., but it might be faster in the long run for a plumber to do it. They know just how to diagnose the problem, where to buy replacement parts, and how to use the tools. If you did it yourself, I'm sure you could figure it out, but you'd be reading books, maybe you buy the wrong part, and your toilet might be out of commission for a while. A plumber is an optimizer that eliminates the wrong plumbing operations.

Conclusions

That's my answer to the question of how React+ClojureScript is faster than MVC and sometimes even faster than React by itself. If you add optimizers that are faster than the operations they're eliminating, it's a performance win. Virtual DOM uses quick checks to eliminate expensive DOM operations. Immutable data uses fast pointer comparison to eliminate expensive whole-tree comparisons. It's not much of a paradox that as things get complicated, the more sophisticated system starts outperforming the brute-force approach.

If you'd like to explore ClojureScript and React (through Om), I recommend the excellent LispCast Single Page Applications with ClojureScript and Om. It's an interactive course with videos, exercises, and lots of source code. It takes you from zero to single page app.