JS: Benchmarking Lazy Getters

Lazily evaluating properties is not just handy, but mandatory in some case. This post explains strategies and provides answers regarding this topic.

Some Background

Where I work, we deal daily with hundred thousand things to eventually parse when needed, and having lazy resolutions, in terms of parsing time, to provide meaningful results on demand is not an option!

The Pattern

JS provides many ways to do the same thing, and that’s both awesome and cumbersome, as we don’t have an opinionated, “always best for the job”, way of doing such things … so we need to explore, which is arguably another great side-effect of using such flexible programming language 🥳

Lazy Override

class LazyOverride {
get value() {
let value = Math.random();
Object.defineProperty(this, 'value', {value});
return value;
}
}

Lazy Known

class LazyKnown {
constructor() {
this._value = null;
}
get value() {
return this._value || (this._value = fn());
}
}
  • the getter will be invoked every single time the property is accessed, as opposite of becoming the property itself, without prototype round-trip.

Object Literal

const getRandom = () => Math.random();function Literal() {
let value = null;
return {
get value() {
return value || (value = getRandom());
}
};
}
  • old school Web developers know that classes were never a thing, and ES2015 ruined the Web introducing these, because good old prototypal inheritance was enough.

Round #1

benchmark for these patterns
  • how long did it take to access that value property for the first time, per each instance?
  • how long did it take, once the property was accessed already, to retrieve back the same data?
  • how long did it take to repeatedly get such data every next execution time?

A first look

The literal idea is the worst ever, as the creation time is 8x classes based, the heap is 5x the one needed for classes/prototypal based instances, and while accessing the value property for the first time wins, repeated access through the scope is slower than instances based inheritance via accessors.

When does this matter

A few consistent results that differ few ms might not feel like a great achievement, if your laptop is “beefy”, so that to have a better idea of what was going on, and what was definitively the best approach, in general, and not only on my hardware, or OS, I decided to run the very same benchmark on a Raspberry Pi 4 with 4GB of RAM: enough to exclude heap issues for this test!

same patterns on a Raspberry Pi
  • Lazy Override, aka shadowed, won in terms of repeatedly property access, and 12ms vs 16ms means concretely nothing in a 200K array of instances
  • Object literals lost almost any advantage whatsoever: heavier on the heap, much slower on creation, slightly faster than shaped instances on first access, but also irrelevantly faster in there for a 200K array

And the winner is …

Among these 3 patterns, the clear winner is the shaped approach: irrelevantly faster, or slower, on instance creation, 2/3rd or 1/4th+ heap needed, and more than twice as fast at finalizing the benchmark … but is this pattern the absolute best one?

About Sub Patterns

The shaped pattern is based on the fact an instance shape is known at construction time, and such property addressed via an _prop underscore prefix convention, that is meaningless for the JS engine, but meaningful for the developer.

  • JS previously offered the Symbol primitive, which is enumerable like any other property, but it grants no name clashing, plus it’s harder to have in our way out there, yet it’s a direct property like _prop would be.

Private Properties

const getRandom = () => Math.random();class LazyGetterPrivate {
#value = null;
get value() {
return this.#value || (this.#value = getRandom());
}
}

Symbol Properties

const VALUE = Symbol('value');const setSymbol = self => {
let value = Math.random();
self[VALUE] = value;
return value;
};
class LazyGetterSymbol {
constructor() {
this[VALUE] = null;
}
get value() {
return this[VALUE] || setSymbol(this);
}
}

Round #2

shaped VS symbol VS private
  • private properties add nothing on the heap side, which is some awesome news, but it offers nothing more in terms of performance, and if the goal is to improve lazy properties access, the extra cost on creation becomes something not-acceptable, as the whole goal here is to be as fast as possible in creation, and eventually slightly slower on property access.

The Verdict

Using symbols shouldn’t scare developers, but I haven’t noticed wide adoption out there. However, LinkeDOM is based on this “name-clashing-less” pattern, and its performance pulverizes any known competitor, so I start thinking we really overlooked at this pattern.

  • don’t overdo lazy accessors, and use any extra pseudo-private property strategy to obtain best performance.
  • don’t believe classes-less JS is faster or better at anything, in general.
  • don’t over-do lazy accessors, and fallback to what you’ve always known, to day, was very fast, and easy to deal with, that being _prefixed properties, or, maybe, Symbol('name') one!

What about other engines?

Both different versions and different engines see the shaped approach win.

jsc / JavaScript Core / WebKit
gjs / SpiderMonkey
NodeJS 12
NodeJS 10
NodeJS 8
NodeJS 6

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.