Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Knockout.js Performance Gotcha #3 - All Bindings Fire Together

| Comments

How bindings are processed

Update: In Knockout 3.0, bindings are now fired independently on a single element, so this is no longer an issue for KO 3.0+.

When using multiple bindings on a single element, it is important to understand how Knockout triggers updates to bindings to avoid potential performance issues. For example, a common binding might look like:

1
<select data-bind="options: choices, value: selectedValue, visible: showChoices"></select>

How are the bindings on this element processed? When Knockout determines that an element has bindings, a computed observable is created to aid in tracking dependencies. Inside the context of this computed observable, Knockout parses the data-bind attribute’s value to determine which bindings to run and the arguments to pass. As the init and update functions for each binding are executed, the computed observable takes care of accumulating dependencies on any observables that have their value accessed.

Here is a simplified flow chart of the binding execution:

There are a couple of important points to understand here:

  • The init function for each binding is only executed once. However, currently this does happen inside the computed observable that was created to track this element’s bindings. This means that you can trigger the binding to run again (only it’s update function) based on a dependency created during initialization. Since, the init function won’t run again, this dependency will likely be dropped on the second execution (unless it was also accessed in the update function).

  • There is currently only one computed observable used to track all of an element’s bindings. This means that the update function for all of an element’s bindings will run again when any of the dependencies are updated.

This is definitely something to consider when writing custom bindings where your update function does a significant amount of work. Whenever bindings are triggered for the element, your update function will run, even if none of its observables were triggered. If possible, it is a good idea to check if you need to do work, before actually executing your update logic.

Common problems with the default bindings

1- A common scenario where this can cause an issue is when using the template binding in conjunction with other bindings. For example, you may attach a visible binding along with a template binding like:

1
<div data-bind="visible: showPlaylist, template: { name: 'playlistTmpl', data: playlist }"></div>

In this case, if showPlaylist is frequently updated, it will cause the template binding to re-render the template again. In some cases, this may not cause a concern (it would just behave like the if binding). However, in a scenario where the template has significant markup and the visible binding’s condition is frequently triggered this can cause an unnecessary performance hit. Note that when using the foreach option or binding, logic is executed to determine if any items were added or removed, so it will cause less of a performance hit in that case.

2- Another place where this can come into play with the default bindings is when using the options and value bindings together. The update function of the options binding rebuilds the list of option tags for the select element. Whenever the value is updated, it will trigger all of the bindings on the element to execute. So, instead of just setting the appropriate value, it will rebuild all of the options and then set the value. If you have a situation where you have a large number of options, then this can cause a performance issue.

Ways to address this concern

1- In some cases, it makes sense to put bindings on separate elements or on a container element. For example, you may be able to move a frequently triggered visible binding to a parent element rather have it coupled with other bindings like the template binding.

1
2
3
<div data-bind="visible: showPlaylist">
    <div data-bind="template: { name: 'playlistTmpl', data: playlist }"></div>
</div>

Here is a sample showing a visible and template binding on the same and different elements:

2- In the case of the options and value bindings, you can choose to build your option elements separately. It would be nice to just use a containerless foreach statement inside of a select element, but Internet Explorer will remove comments that it finds between select and option elements. An alternative would be to use Michael Best’s repeat binding on the option element like:

1
2
3
<select data-bind="value: selectedOption">
    <option data-bind="repeat: {foreach: items, bind: 'attr: { value: $item().id }, text: $item().name'}">
</select>

3- A more advanced way to handle these issues, is to create your own computed observable in the init function to handle updates yourself. Any observables accessed in your computed observable, will not be a dependency of the overall computed observable used to track all of the element’s bindings.

This is a technique that I tend to use by default in any bindings that I write. It is also useful when you want a single binding to accept multiple observable options and you want to respond separately to each one changing (as opposed to using the update function to repond when any observable changes). You can even wrap the existing bindings in this way to create an isolatedOptions binding.

1
2
3
4
5
6
7
8
9
10
11
12
13
ko.bindingHandlers.isolatedOptions = {
    init: function(element, valueAccessor) {
        var args = arguments;
        ko.computed({
            read:  function() {
               ko.utils.unwrapObservable(valueAccessor());
               ko.bindingHandlers.options.update.apply(this, args);
            },
            owner: this,
            disposeWhenNodeIsRemoved: element
        });
    }
};

A few notes on this technique:

  • The idea is that we want to trap our dependencies in our own isolated computed observable.
  • We call the update function of the options binding using .apply with the original arguments that were passed ot the binding.
  • The disposeWhenNodeIsRemoved option ensures that this computed observable will be destroyed if Knockout removes our element, like in a templating scenario.
  • There is one minor issue with using this technique currently: observables that are accessed in the actual binding string are included in the overall computed observable during parsing rather than when you create you call valueAccessor. This means that if your binding contains an expression where you access the observable’s value (text: 'Hello ' + name()) , then it will be tracked in the overall computed observable. This is likely to change in the near future.

Here is a sample that shows using #2 and #3 with options and value bindings: http://jsfiddle.net/rniemeyer/QjVNX/

Future

There has been some thought and work put into running each binding in the context of its own computed observable. Michael Best has this working properly in his Knockout fork. As these changes can be considered breaking, they will likely be carefully implemented over time using an opt-in approach, perhaps until a major version allows us to make some potentially breaking changes. For now, it is wise to keep in mind how your bindings are triggered and how that affects other bindings on the same element.

Comments