Faster, Better DOM manipulation with Dommy and ClojureScript

Last year, we migrated our web development to ClojureScript and haven’t looked back. Over the last few months, we’ve been building out dommy, a ClojureScript library for DOM templating and manipulation. The library is relatively far along and we think it’s time to share our reasons for why we’d write yet another DOM library, rather than use jQuery within ClojureScript. We built dommy because we felt that a ClojureScript DOM library could be a simpler, faster, and better version of jQuery that fits organically into expressive functional code. In this post, we’ll talk about how we made dommy so much faster than jQuery (for example, 15x faster for a simple ID selector) and how we can acheive jQuery-style patterns with less overhead using ClojureScript-native abstractions.

Macros make you much faster

ClojureScript has a compilation step which converts Clojure into JavaScript. During compilation, ClojureScript macros examine your code and can re-write expressions that generate more efficient JavaScript upfront. Macros enable the much of the work the client does at run-time to be moved to compile-time. The differences can be pretty dramatic for common web development tasks, particularly those in performance-sensitive environments like mobile.

Selectors

Probably the most common uses of jQuery are DOM selectors. Most selectors in the wild are simple queries for either an element by ID (e.g., $(‘#some-id’)) or by class (e.g, $(‘.some-class’)). Both jQuery and dommy eventually delegate to fast native DOM methods for such selectors. However in jQuery, the work of parsing the selector string and detecting that is a simple ID selector is done on the client runtime. Dommy, on the other hand, parses the expression at compile-time and rewrites the selector directly to the native DOM method where applicable. Here are some specific examples of how selectors are rewritten:

jQuery, by contrast, incurs significant overhead in parsing the selector at runtime:

While JavaScript as a compilation target has become much more popular with libraries (Google Closure) and languages (CoffeScript and Dart), none of these options have the power of macros to allow clients to control the compilation process. In fact, few languages outside of the LISP family (which includes Clojure) have access to macros for optimizations such as this. Macros are useful for more than just selectors, they also come in handy for another common DOM task: templating.

Templating

We covered dommy macro templating in a past post, but it bears repeating since it’s yet another example of why macros are awesome for client-side performance. Suppose you want to generate DOM for a user profile where each profile displays several user-generated posts. In dommy you might create two small templates for this:

This nested data in this template is compiled straight to efficient Javascript DOM method executions (see here). The jQuery equivalent is much more unwieldy and requires separating out part of the template, because it has to be dynamically generated. It is also 2x slower than dommy (see test here):

Again the time saving comes from macros moving the work of parsing the template data structures from runtime to compile-time and directly generating efficient JavaScript.

Simpler, Better, and Extensible

jQuery has many powerful patterns such as method chaining and being able to seamlesslesly handle either a single or multiple elements. Part of why we wrote dommy was because we could acheive the same level of expressiveness in ClojureScript without the complications and overhead jQuery undergoes to support these features in JavaScript.

Away with DOM wrapper objects

jQuery outputs are typically DOM objects inside a wrapper object. A source of frustration in jQuery is not being sure if you’re dealing with an actual DOM element or jQuery’s wrapper around such an element. There is also a non-trivial performance hit. So what’s the upshot? The jQuery object effectively exists to support chaining of jQuery methods:

In dommy, each of our DOM manipulation functions return plain unwrapped DOM nodes. We use Clojure’s single-threaded arrow macro -> to chain expressions:

In Clojure, the idea of threading an object through a series of functions which update the object and return the updated object can be expressed cleanly using the -> macro. To acheive this pattern in JavaScript, jQuery has to use wrapper object methods (like addClass) instead of just a function to support chaining. There isn’t a better option in JavaScript, but there is in Clojure and dommy leverages that.

One is the loneliest number

Another messy part of jQuery’s semantics are that many (but not all) of the jQuery methods will happily work on one or many things which can be coerced into jQuery DOM objects. The intent is to support processing one or many objects in a similar way. Here’s a concrete example in jQuery:

jQuery’s API has a suite of sequence processing function (filter, sort, etc.) which don’t conceptually belong in jQuery, but are there to compensate for the widespread lack of such in JavaScript. But in order to do the sequence processing, the poor addClass function is forced to operate on both a singleton and a sequence.

In dommy, add-class! only works on a single DOM object, but we still the sequence-processing expressiveness using built-in Clojure language features:

We can even use Clojure’s standard library to do things you can’t easily do with jQuery:

Aside from just yielding a smaller library, dommy’s avoidance of wrapper objects is also a significant performance boost since no runtime cycles are wasted trying to guess what your input objects are and constructing objects to wrap them.

Extensible

Like jQuery, we want our DOM manipulation to accept multiple kinds of things which can be coerced to DOM-like objects. In jQuery, this is limited to a pre-defined set of things (strings representing html, native DOM objects, and jQuery wrapper objects themselves). In dommy, there is a Clojure protocol for what can be converted to a DOM, which any object can implement. We extend that protocol to native Clojure data structures (this is how templating works in dommy). This makes it really easy to extend the DOM manipulation and templating system in client code:

Altogether, dommy’s integration into the Clojure environment lets you treat DOM nodes as first-class objects and protocols:

Are you saying jQuery sucks?

No. jQuery was a massive improvement in web development, but was fundamentally limited by the language that it was implemented in. Dommy is mostly us re-considering how we’d write DOM manipulation and templating in a way that felt Clojure-y and functional native, drawing on a lot of the good choices jQuery did make.

In fairness, jQuery currently has more features and supports a much broader range of browsers. Over the next few months, we plan on increasing dommy’s coverage to cover at least 95% of browsers (somewhere on par with jQuery 2.0’s coverage). Stay tuned for future developments and updates to dommy!


engineering
Prismatic Team |