update: Knockout 2.2 has addressed the first issue in this post. Now the if
and ifnot
binding will not re-render their contents unless the value changes between truthy and falsy.
A recent StackOverflow question prompted me to start a series of short posts with some basic performance tips/gotchas for Knockout.js. In smaller applications, many of these items would go unnoticed, but as a project becomes more complex, it is important to understand the amount of work that Knockout is doing to manage your UI.
The control flow bindings introduced in Knockout 2.0 are really just simple wrappers to the template binding. They use the children of the element as their “template” (what we call anonymous templates) and will re-render the contents whenever triggered. If you are not careful, the if
and with
bindings may be causing re-renders much more often than you might realize.
Problem: if binding causing frequent re-renders
A common scenario where you might use the if binding is when you want to display a section only when an observableArray actually contains items. Something like:
1 2 3 4 5 6 |
|
The problem in this case, is that the if
binding depends on the items
observableArray and re-evaluates every time that it is updated. Each time that it is triggered and has a truthy value, it will actually re-render its “template” again, as you can see here.
Solutions
One choice to avoid these re-renders would be to instead use the visible
binding on a container element around our section or on the individual elements, as shown here.
If we prefer to use the if
binding in this case, then we need to make sure that it is only triggered when the number of items in our array moves between 0 and 1. One might think that a computed observable holding the length would be a good choice. However, one important thing to note is that currently computed observables will always notify subscribers when re-evaluated, even if their value ends up being the same.
However, this is not true for observables. Notifications are suppressed when you write a value that is identical to the previous value (this behavior can be overridden). So, one option is to subscribe to changes to our observableArray and populate an observable based on the length.
1 2 3 4 5 6 7 |
|
Now, we can bind against hasItems
and it will only notify subscribers when changing between true and false based on the length of the array. You can here see that each item is no longer re-rendered on every add.
You could even extend observableArrays to add a function that would attach this hasItems
property as a sub-observable and create the subscription to keep it updated. This helps to keep the view model a bit cleaner and allows you to easily reuse the code.
It might look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Here is a sample of how to use this function.
Problem: with binding causes entire area to re-render
The with binding is very handy for binding against nested objects/models and helps to keep your data-bind attributes from becoming too verbose. It is important, however, to understand that the with binding will re-render the entire section whenever its bound value changes.
For example, we might want to render a selected item like:
1 2 3 4 5 |
|
The children of the main div
become our template. Whenever selectedItem
changes, the contents are re-rendered completely. So, rather than just binding the inputs against the new data, the entire section is replaced with new elements, as shown here. In this case, it won’t cause any issues, but if your markup is complex and/or has events attached outside of Knockout, then you can run into issues when the elements are removed and replaced by the original template.
Solutions
If performance does end up becoming an issue, then we could consider just binding directly to the observables on the selected item. If the selected item might be null, then you would need to make sure that the binding does not fail looking for properties on a null object, as show in this sample.
Another choice would be to use the with
binding in smaller, targeted areas. We can make use of the containerless control-flow bindings to wrap more specific sets of elements, as shown here.
Finally, for cases where you want to bind against a nested object that won’t change (non-observable), take a look at the simple withlight binding developed by Michael Best. This will switch the context to your nested object on initialization, but is not designed to bind against an observable that will be changed.
Conclusion
The control-flow bindings are certainly handy, but when performance matters, it helps to understand a little bit about how they are implemented. In cases where you want to avoid re-rendering entire sections of your UI, it takes a little extra work to ensure that this is not happening too frequently.