I'm just getting started with Knockout.js (always wanted to try it out, but now I finally have an excuse!) - However, I'm running into some really bad performance problems when binding a table to a relatively small set of data (around 400 rows or so).
In my model, I have the following code:
this.projects = ko.observableArray( [] ); //Bind to empty array at startup
this.loadData = function (data) //Called when AJAX method returns
{
for(var i = 0; i < data.length; i++)
{
this.projects.push(new ResultRow(data[i])); //<-- Bottleneck!
}
};
The issue is the for
loop above takes about 30 seconds or so with around 400 rows. However, if I change the code to:
this.loadData = function (data)
{
var testArray = []; //<-- Plain ol' Javascript array
for(var i = 0; i < data.length; i++)
{
testArray.push(new ResultRow(data[i]));
}
};
Then the for
loop completes in the blink of an eye. In other words, the push
method of Knockout's observableArray
object is incredibly slow.
Here is my template:
<tbody data-bind="foreach: projects">
<tr>
<td data-bind="text: code"></td>
<td><a data-bind="projlink: key, text: projname"></td>
<td data-bind="text: request"></td>
<td data-bind="text: stage"></td>
<td data-bind="text: type"></td>
<td data-bind="text: launch"></td>
<td><a data-bind="mailto: ownerEmail, text: owner"></a></td>
</tr>
</tbody>
My Questions:
- Is this the right way to bind my data (which comes from an AJAX method) to an observable collection?
- I expect
push
is doing some heavy re-calc every time I call it, such as maybe rebuilding bound DOM objects. Is there a way to either delay this recalc, or perhaps push in all my items at once?
I can add more code if needed, but I'm pretty sure this is what's relevant. For the most part I was just following Knockout tutorials from the site.
UPDATE:
Per the advice below, I've updated my code:
this.loadData = function (data)
{
var mappedData = $.map(data, function (item) { return new ResultRow(item) });
this.projects(mappedData);
};
However, this.projects()
still takes about 10 seconds for 400 rows. I do admit I'm not sure how fast this would be without Knockout (just adding rows through the DOM), but I have a feeling it would be much faster than 10 seconds.
UPDATE 2:
Per other advice below, I gave jQuery.tmpl a shot (which is natively supported by KnockOut), and this templating engine will draw around 400 rows in just over 3 seconds. This seems like the best approach, short of a solution that would dynamically load in more data as you scroll.
If using IE, try closing the dev tools.
Having the developer tools open in IE significantly slows this operation down. I'm adding ~1000 elements to an array. When having the dev tools open, this takes around 10 seconds and IE freezes over while it is happening. When i close the dev tools, the operation is instant and i see no slow down in IE.
I also noticed that Knockout js template engine works slower in IE, I replaced it with underscore.js, works way faster.
Please see: Knockout.js Performance Gotcha #2 - Manipulating observableArrays
A possible work-around, in combination with using jQuery.tmpl, is to push items on at a time to the observable array in an asynchronous manner, using setTimeout;
This way, when you only add a single item at a time, the browser / knockout.js can take its time to manipulate the DOM accordingly, without the browser being completely blocked for several seconds, so that the user may scroll the list simultaneously.
Use pagination with KO in addition to using $.map.
I had the same problem with a large datasets of 1400 records until I used paging with knockout. Using
$.map
to load the records did make a huge difference but the DOM render time was still hideous. Then I tried using pagination and that made my dataset lighting fast as-well-as more user friendly. A page size of 50 made the dataset much less overwhelming and reduced the number of DOM elements dramatically.Its very easy to do with KO:
http://jsfiddle.net/rniemeyer/5Xr2X/
I've been experimenting with performance, and have two contributions that I hope might be useful.
My experiments focus on the DOM manipulation time. So before going into this, it is definitely worth following the points above about pushing into a JS array before creating an observable array, etc.
But if DOM manipulation time is still getting in your way, then this might help:
1: A pattern to wrap a loading spinner around the slow render, then hide it using afterRender
http://jsfiddle.net/HBYyL/1/
This isn't really a fix for the performance problem, but shows that a delay is probably inevitable if you loop over thousands of items and it uses a pattern where you can ensure you have a loading spinner appear before the long KO operation, then hide it afterwards. So it improves the UX, at least.
Ensure you can load a spinner:
Hide the spinner:
which triggers:
2: Using the html binding as a hack
I remembered an old technique back from when I was working on a set top box with Opera, building UI using DOM manipulation. It was appalling slow, so the solution was to store large chunks of HTML as strings, and load the strings by setting the innerHTML property.
Something similar can be achieved by using the html binding and a computed that derives the HTML for the table as a big chunk of text, then applies it in one go. This does fix the performance problem, but the massive downside is that it severely limits what you can do with binding inside each table row.
Here's a fiddle that shows this approach, together with a function that can be called from inside the table rows to delete an item in a vaguely-KO-like way. Obviously this isn't as good as proper KO, but if you really need blazing(ish) performance, this is a possible workaround.
http://jsfiddle.net/9ZF3g/5/