Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Knockout.js Performance Gotcha #2 - Manipulating observableArrays

| Comments

Most Knockout.js applications tend to deal with collections of objects that are stored in one or more observableArrays. When dealing with observableArrays, it is helpful to understand exactly how they work to make sure that you don’t hit a performance issue.

An observableArray is simply an observable with some additional methods added to deal with common array manipulation tasks. These functions get a reference to the underlying array, perform an action on it, then notify subscribers that the array has changed (in KO 2.0, there is now an additional notification prior to the change as well).

For example, doing a push on an observableArray essentially does this:

1
2
3
4
5
6
7
ko.observableArray.fn.push = function () {
    var underlyingArray = this();
    this.valueWillMutate();
    var result = underlyingArray.push.apply(underlyingArray, arguments);
    this.valueHasMutated();
    return result;
};

Pushing items to an observableArray

Let’s consider a common scenario where we have a collection of objects along with a computed observable to track a filtered array of those objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
var ViewModel = function() {
    this.items = ko.observableArray([
        new Item("Task One", "high"),
        new Item("Task Two", "normal"),
        new Item("Task Three", "high")
    ]);

    this.highPriorityItems = ko.computed(function() {
        return ko.utils.arrayFilter(this.items(), function(item) {
            return item.priority() === "high";
        });
    };
};

Now, say we want to add additional data from the server to the observableArray of items. It is easy enough to loop through the new data and push each mapped item to our observableArray.

1
2
3
4
5
6
7
this.addNewDataBad = function(newData) {
    var item;
    for (var i = 0, j = newData.length; i < j; i++) {
        item = newData[i];
        self.items.push(new Item(item.name, item.priority));
    }
};

Consider what happens though each time that we call .push(). The item is added to our underlying array and any subscribers are notified of the change. Each time that we push, our highPriorityItems filter code will run again. Additionally, if we are binding our UI to the items observableArray, then the template binding has to do work each time to determine that only the one new item was added.

A better pattern is to get a reference to our underlying array, push to it, then call .valueHasMutated(). Now, our subscribers will only receive one notification indicating that the array has changed.

1
2
3
4
5
6
7
8
this.addNewDataGood = function(newData) {
    var item, underlyingArray = self.items();
    for (var i = 0, j = newData.length; i < j; i++) {
        item = newData[i];
        underlyingArray.push(new Item(item.name, item.priority));
    }
    self.items.valueHasMutated();
};

This can be simplified down to:

1
2
3
4
5
6
7
this.addNewData = function(newData) {
    var newItems = ko.utils.arrayMap(newData, function(item) {
       return new Item(item.name, item.priority);
    });
    //take advantage of push accepting variable arguments
    self.items.push.apply(self.items, newItems);
};

Here is a jsFiddle that tracks the number of re-evaluations for each method: http://jsfiddle.net/rniemeyer/NCpxm/.

Clearing or replacing the contents of an observableArray

Another common scenario is when you want to completely replace the contents of an observableArray. This may be to empty the array or to set it to a new set of items. There is no need to loop through the array and operate on it. The most efficient operation is to simply set it to a new value:

1
2
3
4
5
//clear the observableArray
this.items([]);

//replace the contents of the observableArray
this.otherItems(newData);

Minimizing the amount of notifications to subscribers from observableArrays is an easy way to avoid a potential performance issue. Typically, this can be done by performing your operations on the underling array and then triggering notifications. In other cases, replacing the observableArray’s value with a completely new value is a good choice that results in a single notification.

Comments