Defining properties with "lazy values" is a technique you can use to improve performance in Object-Oriented JavaScript. It is especially beneficial because it can improve application load time, which can greatly impact important metrics like bounce rate and revenue.
In this article, we’ll answer:
- What is a Lazy Value?
- How do Lazy Values work?
- Should you use Lazy Values?
What is a Lazy Value?
Normally when a property is created on an object, the expression that defines its value is evaluated eagerly. Eager evaluation means that the getUniqueId
function in the following example is called immediately to compute the value for id
whenever MyObj
is called:
function getUniqueId() {
// some magic to create an ID
}
function MyObj() {
this.id = getUniqueId();
}
var obj1 = new MyObj();
console.log(obj1); // -> { id: 1 }
The getUniqueId
function is called even if the id
property is never used in the rest of the code. Lazy Values avoid this evaluation. Lazy Values are not computed until the first time the property is read:
var obj2 = new MyObjWithLazyId();
console.log(obj2); // -> { }
console.log(obj2.id); // -> 2
console.log(obj2); // -> { id: 2 }
How do Lazy Values work?
The key to setting up Lazy Values is Object.defineProperty. This API lets you define properties and gives you lots of control over how they behave.
To define a Lazy Value, first we define the property as a getter:
Object.defineProperty(MyObjWithLazyId.prototype, 'id', {
get: function() {
return getUniqueId()
}
});
Notice how we're adding this to the
prototype
of ourMyObjWithLazyId
constructor. This allows us to define this property once and have it used by any instance ofMyObjWithLazyId
.
This gets us part of the way there, but with just this, the id
property would change every time it is read:
var obj3 = new MyObjWithLazyId();
console.log(obj3.id); // -> 2
console.log(obj3.id); // -> 3
console.log(obj3.id); // -> 5
console.log(obj3.id); // -> 8
To make this work correctly, we first define the property using a getter, but when the getter is called, we use Object.defineProperty
again to redefine the property as a value:
Object.defineProperty(MyObjWithLazyId.prototype, 'id', {
get: function() {
var id = getUniqueId();
Object.defineProperty(this, 'id', {
value: id
});
return id;
}
});
Redefining the property as a value means that the next time it is read, the value will be returned without any function needing to be called.
Should you use Lazy Values?
The benefit of using Lazy Values is that they don't have to be computed during the initialization of your app. As you can see from this benchmark, the performance of Lazy Values (the orange bar below) is very close to the performance of constructing a completely empty object, which is shown in the blue bar (larger is better):
If you're building a module that will only be used in a few places in your application, the benefits of using this technique are probably outweighed by the complexity you're adding for yourself the next time you read this code. Make sure to performance test in your app to see if the benefits are worth any decrease in readability.
If the code you're writing will be used many times throughout your application or you are building a world-class JavaScript library, it is likely that your users will benefit from the performance improvement Lazy Values provide.
If you want to use this technique, we just published can-define-lazy-value to make it very easy to get started. You can install it from npm and take a look at its 25 lines of code to see exactly how it works.