Knockout.js incredibly slow under semi-large datasets
Asked Answered
F

12

86

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:

  1. Is this the right way to bind my data (which comes from an AJAX method) to an observable collection?
  2. 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.

Foley answered 14/3, 2012 at 20:2 Comment(14)
Are you using a the knockout foreach binding or the template binding with foreach. I'm just wondering if using template and including jquery tmpl instead of the native template engine may make a difference.Lansquenet
@Lansquenet - I added my template to the post. Since I'm new to Knockout, I'm not 100% sure what you mean - Perhaps you can elaborate.Foley
Just a thought.. What browser are you testing this on? If you're on Firefox, turn Firebug off and try again, it can really make these things slowTallis
@Tallis - Yea, unfortunately I'm on IE7 (which is the corporate standard here). I'm sure IE7 doesn't have the world's fastest JavaScript engine, but I still think I can get better results than this. I get roughly the same times with no debuggers attached.Foley
@MikeChristensen - Knockout has it's own native template engine associated with the (foreach, with) bindings. It also supports other template engines, namely jquery.tmpl. Read here for more details. I haven't done any benchmarking with different engines so don't know if it will help. Reading your previous comment, in IE7 you may struggle to get the performance that you are after.Lansquenet
@Lansquenet - I'll check into the template engine as well. I can give it a shot on IE8 just to see how much the script engine plays a role, however 100% of users will be on IE7 so we have to make it work well.Foley
I find IE8 performs slightly better than IE7 but really they are both awful in comparison to the IE9 and real browsers like chrome.Lansquenet
Considering we just got IE7 a few months ago, I think IE9 will be rolled out around summer, 2019. Oh, we're all on WinXP too.. Blech.Foley
@Lansquenet - jQuery.tmpl seems to work great! If you add this as an answer, I'll mark is as accepted.Foley
@MikeChristensen - I wonder if deferred updates would improve the performance here. github.com/mbest/knockout-deferred-updatesTwofold
p.s., The reason it seems slow is that you're adding 400 items to that observable array individually. For every change to the observable, the view has to be rerendered for anything that depends on that array. For complex templates and many items to add, that's a lot of overhead when you could have just updated the array all at once by setting it to a different instance. At least then, the rerendering would be done once.Dynast
I found a way which is faster and neat (nothing out of box) . using valueHasMutated does it . check the answer if you got time .Tessellation
@supercool - Wow, I was looking for something like that! I'm at a different job now and I no longer do any JavaScript stuff heh.Foley
ha ha @MikeChristensen cool . If you ever end up doing ko again take a shot at it .Tessellation
L
16

As suggested in the comments.

Knockout has it's own native template engine associated with the (foreach, with) bindings. It also supports other template engines, namely jquery.tmpl. Read here for more details. I haven't done any benchmarking with different engines so don't know if it will help. Reading your previous comment, in IE7 you may struggle to get the performance that you are after.

As an aside, KO supports any js templating engine, if someone has written the adapter for it that is. You may want to try others out there as jquery tmpl is due to be replaced by JsRender.

Lansquenet answered 15/3, 2012 at 16:16 Comment(7)
I'm getting much better perf with jquery.tmpl so I'll use that. I might investigate other engines as well as writing my own if I have some extra time. Thanks!Foley
@MikeChristensen - are you still using data-bind statements in your jQuery template, or are you using the ${ code } syntax?Vortical
@Vortical - With the new code, I'm using ${code} syntax and it's much faster. I've also been trying to get Underscore.js working, but haven't had any luck yet (the <% .. %> syntax interferes with ASP.NET), and there doesn't yet seem to be JsRender support.Foley
@MikeChristensen - ok, then this makes sense. KO's native template engine isn't necessarily that inefficient. When you use the ${ code } syntax, your not getting any data-binding on those elements (which improves perf). Thus if you change a property of a ResultRow, it won't update the UI (you'll have to update the projects observableArray which will force a re-rendering of your table). ${ } can definitely be advantageous if your data is pretty much read-onlyVortical
@MikeChristensen : Not to interfere with ASP.NET, I guess you could change underscores' delimeters : underscorejs.org/#templateApul
Necromancy! jquery.tmpl is no longer in developmentCloy
@AlexLarzelere: So what would be the best option now?Dogfish
S
50

Please see: Knockout.js Performance Gotcha #2 - Manipulating observableArrays

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.

Solent answered 12/10, 2012 at 17:2 Comment(0)
L
16

As suggested in the comments.

Knockout has it's own native template engine associated with the (foreach, with) bindings. It also supports other template engines, namely jquery.tmpl. Read here for more details. I haven't done any benchmarking with different engines so don't know if it will help. Reading your previous comment, in IE7 you may struggle to get the performance that you are after.

As an aside, KO supports any js templating engine, if someone has written the adapter for it that is. You may want to try others out there as jquery tmpl is due to be replaced by JsRender.

Lansquenet answered 15/3, 2012 at 16:16 Comment(7)
I'm getting much better perf with jquery.tmpl so I'll use that. I might investigate other engines as well as writing my own if I have some extra time. Thanks!Foley
@MikeChristensen - are you still using data-bind statements in your jQuery template, or are you using the ${ code } syntax?Vortical
@Vortical - With the new code, I'm using ${code} syntax and it's much faster. I've also been trying to get Underscore.js working, but haven't had any luck yet (the <% .. %> syntax interferes with ASP.NET), and there doesn't yet seem to be JsRender support.Foley
@MikeChristensen - ok, then this makes sense. KO's native template engine isn't necessarily that inefficient. When you use the ${ code } syntax, your not getting any data-binding on those elements (which improves perf). Thus if you change a property of a ResultRow, it won't update the UI (you'll have to update the projects observableArray which will force a re-rendering of your table). ${ } can definitely be advantageous if your data is pretty much read-onlyVortical
@MikeChristensen : Not to interfere with ASP.NET, I guess you could change underscores' delimeters : underscorejs.org/#templateApul
Necromancy! jquery.tmpl is no longer in developmentCloy
@AlexLarzelere: So what would be the best option now?Dogfish
Y
13

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/

Yseulta answered 27/8, 2012 at 5:32 Comment(0)
V
11

KnockoutJS has some great tutorials, particularly the one about loading and saving data

In their case, they pull data using getJSON() which is extremely fast. From their example:

function TaskListViewModel() {
    // ... leave the existing code unchanged ...

    // Load initial state from server, convert it to Task instances, then populate self.tasks
    $.getJSON("/tasks", function(allData) {
        var mappedTasks = $.map(allData, function(item) { return new Task(item) });
        self.tasks(mappedTasks);
    });    
}
Verger answered 14/3, 2012 at 20:4 Comment(3)
Definitely a big improvement, but the self.tasks(mappedTasks) takes about 10 seconds to run (with 400 rows). I feel this is still not acceptable.Foley
I'll agree that 10 seconds is not acceptable. Using knockoutjs, I'm not sure what is better than a map, so I'll favorite this question and watch for a better answer.Verger
Ok. The answer definitely deserves a +1 for both simplifying my code and increasing the speed dramatically. Perhaps someone has a more detailed explanation of what the bottleneck is.Foley
V
9

Give KoGrid a look. It intelligently manages your row rendering so that it's more performant.

If you you're trying to bind 400 rows to a table using a foreach binding, you're going to have trouble pushing that much through KO into the DOM.

KO does some very interesting things using the foreach binding, most of which are very good operations, but they do start to break down on perf as the size of your array grows.

I've been down the long dark road of trying to bind large data-sets to tables/grids, and you end up needing to break apart/page the data locally.

KoGrid does this all. Its been built to only render the rows that the viewer can see on the page, and then virtualize the other rows until they are needed. I think you'll find its perf on 400 items to be much better than you're experiencing.

Vortical answered 15/3, 2012 at 13:8 Comment(5)
This appears to be completely broken on IE7 (none of the samples work), otherwise this would be great!Foley
Glad to look into it - KoGrid is still in active development. However, does this at least answer your question regarding perf?Vortical
Yup! It confirms my original suspicion that the default KO template engine is quite slow. If you need anyone to guinea pig KoGrid for you, I'd be happy to. Sounds like exactly what we need!Foley
Darn. This looks really good! Unfortunately, over 50% of my application's users use IE7!Solent
Interesting, nowadays we have to reluctantly support IE11. Things have improved in the last 7ish years.Chace
E
5

A solution to avoid locking up the browser when rendering a very large array is to 'throttle' the array such that only a few elements get added at a time, with a sleep in between. Here's a function which will do just that:

function throttledArray(getData) {
    var showingDataO = ko.observableArray(),
        showingData = [],
        sourceData = [];
    ko.computed(function () {
        var data = getData();
        if ( Math.abs(sourceData.length - data.length) / sourceData.length > 0.5 ) {
            showingData = [];
            sourceData = data;
            (function load() {
                if ( data == sourceData && showingData.length != data.length ) {
                    showingData = showingData.concat( data.slice(showingData.length, showingData.length + 20) );
                    showingDataO(showingData);
                    setTimeout(load, 500);
                }
            })();
        } else {
            showingDataO(showingData = sourceData = data);
        }
    });
    return showingDataO;
}

Depending on your use case, this could result in massive UX improvement, as the user might only see the first batch of rows before having to scroll.

Evonneevonymus answered 25/10, 2013 at 15:21 Comment(1)
I like this solution, but rather than setTimeout every iteration, I recommend only running setTimout every 20 or more iterations because every time also takes too long to load. I see that you are doing that with the +20, but it was not obvious for me at first glance.Griffey
U
5

Taking advantage of push() accepting variable arguments gave the best performance in my case. 1300 rows were loading for 5973ms (~ 6 sec.). With this optimization the load time was down to 914ms (< 1 sec.)
That's 84.7 % improvement!

More info at Pushing items to an observableArray

this.projects = ko.observableArray( [] ); //Bind to empty array at startup

this.loadData = function (data) //Called when AJAX method returns
{
   var arrMappedData = ko.utils.arrayMap(data, function (item) {
       return new ResultRow(item);
   });
   //take advantage of push accepting variable arguments
   this.projects.push.apply(this.projects, arrMappedData);
};
Ubiquitous answered 25/4, 2014 at 14:50 Comment(0)
T
4

I been dealing with such huge volumes of data coming in for me valueHasMutated worked like a charm .

View Model :

this.projects([]); //make observableArray empty --(1)

var mutatedArray = this.projects(); -- (2)

this.loadData = function (data) //Called when AJAX method returns
{
ko.utils.arrayForEach(data,function(item){
    mutatedArray.push(new ResultRow(item)); -- (3) // push to the array(normal array)  
});  
};
 this.projects.valueHasMutated(); -- (4) 

After calling (4) array data will be loaded into required observableArray which is this.projects automatically .

if you got time have a look at this and just in-case any trouble let me know

Trick here : By doing like this , if in case of any dependencies (computed,subscribes etc) can be avoided at push level and we can make them execute at one go after calling (4).

Tessellation answered 2/12, 2014 at 15:14 Comment(1)
The problem isn't too many calls to push, the problem is that even a single call to push will cause long render times. If an array has 1000 items bound to a foreach, pushing a single item rerenders the entire foreach and you pay a large render time cost.Maricruzmaridel
G
1

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;

var self = this,
    remaining = data.length;

add(); // Start adding items

function add() {
  self.projects.push(data[data.length - remaining]);

  remaining -= 1;

  if (remaining > 0) {
    setTimeout(add, 10); // Schedule adding any remaining items
  }
}

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.

Gillespie answered 9/10, 2012 at 7:30 Comment(5)
This will force N number of DOM updates which will result in a total rendering time which is much longer than doing everything at once.Payton
That is of course correct. The point is, however, that the combination of N being a big number and pushing an item into the projects array triggering a significant amount of other DOM updates or computations, may cause the browser to freeze up and offer you to kill the tab. By having a timeout, either per item or per 10, 100 or some other number of items, the browser will still be responsive.Gillespie
I would say that this is the wrong approach in the general case where the total update would not freeze the browser but it is something to use when all other fails. To me it sounds like a badly written application where the performance issues should be solved instead of just making it not freeze.Payton
Of course it is the wrong approach in the general case, no one would disagree with you in that. This is a hack and a proof-of-concept for preventing browser freeze if you need to do loads of DOM operations. I needed it a couple of years back when listing several large HTML tables with several bindings per cell, resulting in thousands of bindings being evaluated, each affecting the state of the DOM. The functionality was needed temporarily, for verifying the correctness of the re-implementation of a Excel-based desktop application as a web application. Then this solution worked out perfectly.Gillespie
The comment was mostly for others to read to not assume that this was the preferred way. I assumed that you knew what you were doing.Payton
R
1

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:

// Show the spinner immediately...
$("#spinner").show();

// ... by using a timeout around the operation that causes the slow render.
window.setTimeout(function() {
    ko.applyBindings(vm)  
}, 1)

Hide the spinner:

<div data-bind="template: {afterRender: hide}">

which triggers:

hide = function() {
    $("#spinner").hide()
}

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/

Retouch answered 23/7, 2014 at 15:15 Comment(0)
E
0

I also noticed that Knockout js template engine works slower in IE, I replaced it with underscore.js, works way faster.

Engle answered 29/7, 2014 at 2:31 Comment(4)
How did you do this please?Execration
@StuHarper I imported underscore library and then in main.js I followed the steps described underscore integration section of knockoutjs.com/documentation/template-binding.htmlEngle
Which version of IE did this improvement occur with?Hasson
@Hasson I was using IE 10, 11.Engle
S
0

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.

Sochi answered 1/3, 2016 at 11:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.