Avoid Converting JavaScript Objects

November 9, 2017

Frequently, when writing ClojureScript that interoperates with JavaScript APIs that accept or return JavaScript objects, it is tempting to reach for clj->js or js->clj and rely on Clojure's rich standard library, operating on persistent values for your data manipulation needs.

Depending on the access patterns involved, this may not be necessary. Oftentimes simpler, more efficient solutions can be crafted using direct interop.




Assume, for example, that an API returns a JavaScript object obj that looks like this

{foo: {entries: [{prop: "alpha"},
                 {prop: "beta"}]}}

and that you want to retrieve the "beta" value.

Your initial reaction might be to first convert the object to a ClojureScript persistent value (using keywords as keys in maps) and then fetch the desired value using get-in:

(let [m (js->clj obj :keywordize-keys true)]
  (get-in m [:foo :entries 1 :prop]))

This is great. But, if your code is not going to do much with the resulting persistent value other than retrieve sub-values, an alternative to consider is the facilities in goog.object. In particular:

Think of goog.object/getValueByKeys as the JavaScript analog of get-in.

Avoiding the js->clj conversion, the above code could instead be simply written:

(goog.object/getValueByKeys obj 
  #js ["foo" "entries" 1 "prop"])

IMHO, two aspects make this get-in analogy work: You can pass an array for the keys, and you can use numbers as indices for array-like objects.

I find this code nearly as easy to read, and as a bonus, it is much more efficient. Here are the speedups I see when using the built-in ClojureScript benchmarking utility simple-benchmark on :advanced-optimized code:

V8: 68, SpiderMonkey: 26, JavaScriptCore: 67, Nashorn: 32, ChakraCore: 56

This means, for example, with V8 the benchmark completes in 103 ms vs. 7022 ms.

If you are curious, setting :keywordize-keys false and instead using strings, things run a little faster, with a speedup of about 1.1 across the major engines. In other words, it is fundamentally the use of js->clj which is slow, not the use of keywords.

The benchmarks are include at the bottom of this post, so you can try them in your favorite ClojureScript REPL.

A couple of side notes:

  1. While (aget obj "foo" "entries" 1 "prop") works, don't do it.
  2. While (-> obj .-foo .-entries (aget 1) .-prop) emits code that works with :none, it can break under :advanced. I really like David Nolen's summarization that goog.object is appropriate when working with JavaScript data.

Conversely, when calling a JavaScript API which requires an object as an argument, consider using #js over clj->js, especially if you don't have a need to first “build up” or otherwise calculate the object as a persistent value (by using things like assoc or merge). In concrete terms, instead of

(clj->js {:foo {:entries [{:prop "alpha"} 
                          {:prop "beta"}]}})

prefer the more direct

#js {:foo #js {:entries #js [#js {:prop "alpha"} 
                             #js {:prop "beta"}]}}

In this case, using #js causes the ClojureScript compiler to directly emit a JavaScript object literal into the code, where the clj->js variant emits code that first constructs a persistent value, and then calls clj->js on it. For comparison, here are the benchmarks.

V8: 8, SpiderMonkey: 44, JavaScriptCore: 44, Nashorn: 74, ChakraCore: 197

Of course, the examples above comprise literal data, but you can easily include references to values in forms making use of #js. Oftentimes, this works out nicely, where the shape of the object needing to be passed to a JavaScript API is essentially static, but where the conveyed leaf values can vary.

In summary, if you have a ClojureScript app that is doing heavy JavaScript interop, if at all possible, avoid unnecessarily converting between JavaScript objects and persistent values. Instead of “clogging up” your runtime with clj->js and js->clj conversions, rely on ClojureScript's inherent interop facilities, and lean on the facilities in goog.object (or cljs-oops if you'd prefer a library that exposes an ClojureScript API that strives to be idiomatic.)

Benchmarks

(simple-benchmark [obj #js {:foo #js {:entries #js [#js {:prop "alpha"} #js {:prop "beta"}]}}]
  (let [m (js->clj obj :keywordize-keys true)]
    (get-in m [:foo :entries 1 :prop])) 1000000)
(simple-benchmark [obj #js {:foo #js {:entries #js [#js {:prop "alpha"} #js {:prop "beta"}]}}]
  (goog.object/getValueByKeys obj
    #js ["foo" "entries" 1 "prop"]) 1000000)
(simple-benchmark [] (clj->js {:foo {:entries [{:prop "alpha"} {:prop "beta"}]}}) 1000000)
(simple-benchmark [] #js {:foo #js {:entries #js [#js {:prop "alpha"} #js {:prop "beta"}]}} 1000000)

Note: While goog.object is already implicitly required by the ClojureScript runtime, you should (require 'goog.object) for correctness, or in production code, perhaps in your ns form, include (:require [goog.object :as gobj])

Tags: ClojureScript