web-dev-qa-db-ja.com

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ループは瞬く間に完了します。つまり、KnockoutのPushオブジェクトの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. バインドされたDOMオブジェクトを再構築するなど、Pushが呼び出すたびに重い再計算を行うことを期待しています。この再計算を遅らせる、またはすべてのアイテムを一度にプッシュする方法はありますか?

必要に応じてコードを追加できますが、これが関連するものであると確信しています。ほとんどの場合、私はサイトからKnockoutチュートリアルをたどっていました。

更新:

以下のアドバイスに従って、コードを更新しました。

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

ただし、this.projects()は400行に対して約10秒かかります。私はこれがどれくらい速くなるかわからないことを認めていますwithoutノックアウト(DOMを介して行を追加するだけです)が、 10秒よりはるかに高速です。

更新2:

以下の他のアドバイスに従って、私はjQuery.tmplショット(KnockOutによってネイティブにサポートされています)を与えました。このテンプレートエンジンは、わずか3秒で約400行を描画します。これは最良のアプローチのように思えますが、スクロールするとより多くのデータを動的にロードするソリューションはありません。

86

コメントで示唆されているように。

Knockoutには、(foreach、with)バインディングに関連付けられた独自のネイティブテンプレートエンジンがあります。また、他のテンプレートエンジン、つまりjquery.tmplもサポートしています。詳細については here を参照してください。さまざまなエンジンでベンチマークを行っていないので、役立つかどうかわかりません。 IE7では、以前のコメントを読んで、目的のパフォーマンスを得るのに苦労する場合があります。

余談ですが、KOは、誰かがそれ用のアダプターを作成した場合、jsテンプレートエンジンをサポートします。 jquery tmplは JsRender に置き換えられる予定なので、他の人も試してみてください。

16
madcapnmckay

参照してください: Knockout.js Performance Gotcha#2-Manipulating observableArrays

より良いパターンは、基になる配列への参照を取得し、それにプッシュしてから、.valueHasMutated()を呼び出すことです。これで、サブスクライバーは、配列が変更されたことを示す1つの通知のみを受け取ります。

50
Jim G.

$。mapの使用に加えて、KOでpaginationを使用します。

ノックアウトでページングを使用するまで、1400レコードの大規模なデータセットで同じ問題が発生しました。 $.mapを使用してレコードをロードすると大きな違いが生じましたが、DOMレンダリング時間は依然として恐ろしいものでした。次に、ページネーションを使用してみました。これにより、データセットのライティングが高速になり、ユーザーフレンドリーになりました。ページサイズが50であるため、データセットの圧倒的な負担が軽減され、DOM要素の数が大幅に削減されました。

KOを使用するのは非常に簡単です。

http://jsfiddle.net/rniemeyer/5Xr2X/

13
Tim Santeford

KnockoutJSには、特に データのロードと保存に関するもの の優れたチュートリアルがあります。

彼らの場合、getJSON()を使用してデータをプルしますが、これは非常に高速です。彼らの例から:

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

KoGrid を見てください。行のレンダリングをインテリジェントに管理して、パフォーマンスを向上させます。

foreachバインディングを使用して400行をテーブルにバインドしようとすると、KOを介してDOMにプッシュするのに問題が発生します。

KOはforeachバインディングを使用していくつかの非常に興味深いことを行いますが、そのほとんどは非常に優れた操作ですが、配列のサイズが大きくなるとパフォーマンスが低下し始めます。

大規模なデータセットをテーブル/グリッドにバインドしようとする長い暗い道を歩んできたので、ローカルでデータを分割/ページングする必要があります。

KoGrid はこれをすべて行います。ビューアーがページ上で見ることができる行のみをレンダリングし、必要になるまで他の行を仮想化するように構築されています。 400アイテムのパフォーマンスは、あなたが経験しているよりもはるかに優れていると思います。

9
ericb

非常に大きな配列をレンダリングするときにブラウザがロックアップするのを回避するための解決策は、配列を「スロットル」して、一度に数個の要素のみを追加し、その間にスリープ状態にすることです。これを行う関数は次のとおりです。

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

ユースケースによっては、ユーザーがスクロールする前に最初のバッチ行のみを表示する場合があるため、これにより、UXが大幅に改善される可能性があります。

5
teh_senaus

私の場合、変数引数を受け入れるPush()を利用すると最高のパフォーマンスが得られました。 1300行が5973ms(〜6秒)でロードされました。この最適化により、ロード時間は914ms(<1秒)に短縮されました。
それは84.7%の改善です!

詳細は 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);
};
5
mitaka

私はvalueHasMutatedが入ってくるこのような膨大な量のデータを扱うのが魅力的でした。

モデルの表示:

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) 

(4)を呼び出した後、配列データはthis.projectsである必要なobservableArrayに自動的にロードされます。

時間があるならこれを見て、万が一のトラブルがあれば知らせてください

Trick here:このようにすることで、Pushレベルで依存関係(計算、購読など)を回避でき、(4)を呼び出した後に一度に実行できるようになります。

4
super cool

可能な回避策は、jQuery.tmplを使用することと組み合わせて、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
  }
}

このように、一度に1つの項目のみを追加する場合、ブラウザー/knockout.jsは、ブラウザーが数秒間完全にブロックされることなく、それに応じてDOMを操作するのに時間がかかるため、ユーザーは同時にリストをスクロールできます。

1
gnab

私はパフォーマンスを試してきましたが、2つの貢献があり、役に立つと思います。

私の実験は、DOM操作時間に焦点を当てています。したがって、これに入る前に、観測可能な配列などを作成する前にJS配列にプッシュすることに関する上記のポイントに従うことは間違いなく価値があります。

しかし、DOM操作時間が依然として邪魔になる場合は、これが役立つ場合があります。


1:ロードスピナーを低速のレンダリングの周りにラップし、afterRenderを使用して非表示にするパターン

http://jsfiddle.net/HBYyL/1/

これは実際にはパフォーマンスの問題を修正するものではありませんが、数千のアイテムをループする場合はおそらく遅延が避けられないことを示しています。その後。したがって、少なくともUXは改善されます。

スピナーをロードできることを確認します。

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

スピナーを非表示にします。

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

どのトリガー:

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

2:HTMLバインディングをハックとして使用する

Operaでセットトップボックスに取り組んでいたときから、DOM操作を使用してUIを構築していた昔のテクニックを思い出しました。ひどく遅いので、ソリューションはHTMLの大きな塊を文字列として保存し、innerHTMLプロパティを設定して文字列をロードすることでした。

同様のことは、htmlバインディングと、テーブルのHTMLを大きなテキストの塊として導出し、一度に適用する計算を使用することで実現できます。これによりパフォーマンスの問題は解決しますが、大きなマイナス面は、各テーブル行内のバインディングでできることを厳しく制限することです。

以下に、このアプローチを示すフィドルと、テーブル行の内側から呼び出して、漠然とKOのような方法でアイテムを削除できる関数を示します。明らかに、これは適切なKOほど良いものではありませんが、非常に優れたパフォーマンスが本当に必要な場合は、回避策が考えられます。

http://jsfiddle.net/9ZF3g/5/

1
sifriday

また、IEでKnockout jsテンプレートエンジンの動作が遅いことに気付きました。

0
Marcello

IEを使用している場合は、開発ツールを閉じてみてください。

開発ツールをIEで開くと、処理が大幅に遅くなります。配列に〜1000個の要素を追加します。開発ツールを開くと、約10秒かかり、IE実行中にフリーズします。開発ツールを閉じると、操作は即座に行われ、IEの速度は低下しません。

0
Jon List