Programing

Knockout.js는 준 대형 데이터 세트에서 엄청나게 느립니다.

lottogame 2020. 9. 14. 21:36
반응형

Knockout.js는 준 대형 데이터 세트에서 엄청나게 느립니다.


이제 막 Knockout.js를 시작하고 있습니다 (항상 사용해보고 싶었지만 이제는 변명 할 수 있습니다!)-그러나 테이블을 상대적으로 작은 집합에 바인딩 할 때 정말 나쁜 성능 문제가 발생합니다. 데이터 (약 400 행 정도).

내 모델에는 다음 코드가 있습니다.

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!
   }
};

문제는 for루프가 약 400 행으로 약 30 초 정도 걸린다는 것입니다. 그러나 코드를 다음과 같이 변경하면

this.loadData = function (data)
{
   var testArray = []; //<-- Plain ol' Javascript array
   for(var i = 0; i < data.length; i++)
   {
      testArray.push(new ResultRow(data[i]));
   }
};

그런 다음 for눈 깜짝 할 사이에 루프가 완료됩니다. 즉, pushKnockout의 observableArray개체 방법 이 엄청나게 느립니다.

내 템플릿은 다음과 같습니다.

<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>

내 질문 :

  1. 이것이 내 데이터 (AJAX 메서드에서 가져온)를 관찰 가능한 컬렉션에 바인딩하는 올바른 방법입니까?
  2. push바운드 DOM 개체를 다시 빌드하는 것과 같이 호출 할 때마다 무거운 재 계산을 수행 할 것으로 예상 합니다. 이 재 계산을 지연하거나 한 번에 모든 항목을 푸시 할 수있는 방법이 있습니까?

필요한 경우 더 많은 코드를 추가 할 수 있지만 이것이 관련성이 있다고 확신합니다. 대부분의 경우 사이트에서 Knockout 튜토리얼을 따랐습니다.

최신 정보:

아래 조언에 따라 코드를 업데이트했습니다.

this.loadData = function (data)
{
   var mappedData = $.map(data, function (item) { return new ResultRow(item) });
   this.projects(mappedData);
};

그러나 this.projects()여전히 400 행의 경우 약 10 초가 걸립니다. Knockout (DOM을 통해 행 추가) 없이 이것이 얼마나 빠를 지 확신 할 수 없지만 10 초보다 훨씬 빠를 것 같습니다.

업데이트 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.


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.


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.


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/


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);
    });    
}

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.


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.


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);
};

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).


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.


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/


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


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.

참고URL : https://stackoverflow.com/questions/9709374/knockout-js-incredibly-slow-under-semi-large-datasets

반응형