Knockout.jsは、中規模のデータセットでは非常に遅くなります


86

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

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

更新:

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

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

ただし、this.projects()400行の場合でも約10秒かかります。ノックアウトなし(DOMを介して行を追加するだけ)でこれがどれほど速くなるかはわかりませんが、10秒よりもはるかに速いと感じています。

更新2:

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


1
ノックアウトforeachバインディングを使用していますか、それともforeachでテンプレートバインディングを使用していますか。テンプレートを使用し、ネイティブテンプレートエンジンの代わりにjquerytmplを含めると違いが生じるのではないかと思っています。
madcapnmckay 2012年

1
@ MikeChristensen-Knockoutには、(foreach、with)バインディングに関連付けられた独自のネイティブテンプレートエンジンがあります。また、他のテンプレートエンジン、つまりjquery.tmplもサポートしています。詳細については、こちらをお読みください。私はさまざまなエンジンでベンチマークを行ったことがないので、それが役立つかどうかわかりません。以前のコメントを読んで、IE7ではあなたが求めているパフォーマンスを得るのに苦労するかもしれません。
madcapnmckay 2012年

2
数か月前にIE7を入手したばかりであることを考えると、IE9は2019年の夏頃に展開されると思います。ああ、私たち全員がWinXPを使用しています。Blech。
マイククリステンセン

1
ps、遅いように見える理由は、400個のアイテムをその監視可能な配列に個別に追加しているためです。オブザーバブルへの変更ごとに、その配列に依存するすべてのものに対してビューを再レンダリングする必要があります。複雑なテンプレートや追加する多くのアイテムの場合、配列を別のインスタンスに設定することで配列を一度に更新できた場合、これは多くのオーバーヘッドになります。少なくともその場合、再レンダリングは1回実行されます。
ジェフメルカード2012

1
私はより速くてきれいな方法を見つけました(箱から出して何もありません)。使用valueHasMutatedします。時間があれば答えを確認してください。
超クールな

回答:


16

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

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

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


パフォーマンスが大幅に向上しているjquery.tmplので、それを使用します。時間があれば、他のエンジンを調べたり、自分でエンジンを書いたりするかもしれません。ありがとう!
マイククリステンセン

1
@ MikeChristensen-まだdata-bindjQueryテンプレートでステートメントを使用していますか、それとも$ {code}構文を使用していますか?
ericb 2012年

@ ericb-新しいコードでは、${code}構文を使用しており、はるかに高速です。私もUnderscore.jsを機能させようとしていますが、まだ運がなく(<% .. %>構文がASP.NETに干渉します)、JsRenderがまだサポートされていないようです。
マイククリステンセン

1
@ MikeChristensen-わかりました、それならこれは理にかなっています。KOのネイティブテンプレートエンジンは、必ずしもそれほど非効率的ではありません。$ {code}構文を使用すると、これらの要素でデータバインディングが取得されなくなります(これによりパフォーマンスが向上します)。したがって、のプロパティを変更してもResultRow、UIは更新されません(projectsテーブルの再レンダリングを強制するobservableArrayを更新する必要があります)。$ {}は、データがほとんど読み取り専用である場合に間違いなく有利です
ericb 2012年

4
ネクロマンシー!jquery.tmplは開発中ではありません
Alex Larzelere 2013年


13

ページネーションを使用する$ .mapの使用に加えて、KOでをます。

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

KOで行うのは非常に簡単です。

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


11

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

1
間違いなく大きな改善ですが、self.tasks(mappedTasks)実行には約10秒かかります(400行)。これはまだ受け入れられないと思います。
マイククリステンセン

10秒は受け入れられないことに同意します。knockoutjsを使用すると、マップよりも優れているものがわからないため、この質問を気に入って、より良い回答を探します。
deltree 2012年

1
OK。答えは間違いなく+1私のコードを単純化することと劇的に速度を上げることの両方に値します。おそらく誰かがボトルネックが何であるかについてより詳細な説明を持っています。
マイククリステンセン

9

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

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

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

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

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


1
これはIE7では完全に壊れているようです(どのサンプルも機能しません)、そうでなければこれは素晴らしいことです!
マイククリステンセン

調べてよかった-KoGridはまだ活発に開発中です。しかし、これは少なくともパフォーマンスに関するあなたの質問に答えますか?
ericb 2012年

1
うん!これは、デフォルトのKOテンプレートエンジンが非常に遅いという私の最初の疑いを裏付けています。KoGridをモルモットにする人が必要な場合は、喜んで対応させていただきます。まさに私たちが必要としているもののように聞こえます!
マイククリステンセン

くそー。これは本当によさそうだ!残念ながら、私のアプリケーションのユーザーの50%以上がIE7を使用しています。
ジムG.

興味深いことに、今日、私たちはしぶしぶIE11をサポートする必要があります。過去7年間で状況は改善しました。
MrBoJangles

5

非常に大きな配列をレンダリングするときにブラウザがロックされないようにするための解決策は、一度に少数の要素のみが追加されるように配列を「スロットル」し、その間にスリープを設定することです。これがまさにそれを行う関数です:

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が大幅に向上する可能性があります。


私はこのソリューションが好きですが、毎回のsetTimeoutではなく、毎回の読み込みに時間がかかりすぎるため、20回以上の反復ごとにsetTimoutのみを実行することをお勧めします。+20でそれを行っているようですが、一見しただけではわかりませんでした。
charlierlee

5

変数引数を受け入れる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);
};

4

入ってくる膨大な量のデータを扱って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にロードされます。

時間があればこれを見て、万が一の場合に備えて私に知らせてください

ここでのトリック:このようにすることで、依存関係(計算、サブスクライブなど)が発生した場合にプッシュレベルで回避でき、呼び出し後に一度に実行させることができ(4)ます。


1
問題はpush、への呼び出しが多すぎることではありません。問題は、プッシュを1回呼び出すだけでも、レンダリング時間が長くなることです。配列に1000個のアイテムがバインドされているforeach場合、1つのアイテムをプッシュすると、foreach全体が再レンダリングされ、レンダリング時間のコストが高くなります。
2016

1

考えられる回避策は、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を操作するのに時間がかかるため、ユーザーはリストを同時にスクロールできます。


2
これにより、N個のDOM更新が強制され、すべてを一度に実行するよりもはるかに長い合計レンダリング時間が発生します。
Fredrik C

もちろんそれは正しいです。ただし、重要なのは、Nが大きい数であり、アイテムをプロジェクト配列にプッシュすると、他の大量のDOM更新または計算がトリガーされるため、ブラウザーがフリーズし、タブを強制終了する可能性があるということです。アイテムごと、10、100、またはその他の数のアイテムごとにタイムアウトを設定することにより、ブラウザーは引き続き応答します。
gnab 2015年

2
これは、更新全体でブラウザがフリーズしない一般的なケースでは間違ったアプローチだと思いますが、他のすべてが失敗したときに使用するものです。私には、フリーズしないようにするのではなく、パフォーマンスの問題を解決する必要がある、ひどく書かれたアプリケーションのように聞こえます。
Fredrik C

1
もちろん、それは一般的なケースでは間違ったアプローチであり、その点で誰もあなたに反対することはありません。これは、大量のDOM操作を実行する必要がある場合に、ブラウザーのフリーズを防ぐためのハックであり、概念実証です。数年前、セルごとにいくつかのバインディングを持ついくつかの大きなHTMLテーブルをリストするときに必要でした。その結果、何千ものバインディングが評価され、それぞれがDOMの状態に影響を与えました。この機能は、ExcelベースのデスクトップアプリケーションをWebアプリケーションとして再実装することの正確さを検証するために一時的に必要でした。その後、このソリューションは完全に機能しました。
gnab 2015年

コメントは主に、これが好ましい方法であると想定しないために他の人が読むためのものでした。私はあなたが何をしているのか知っていると思いました。
Fredrik C

1

私はパフォーマンスを実験してきましたが、役立つと思われる2つの貢献があります。

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

しかし、DOM操作時間がまだ邪魔になっている場合は、これが役立つ可能性があります。


1:読み込み中のスピナーを遅いレンダリングの周りにラップし、afterRenderを使用して非表示にするパターン

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

これは実際にはパフォーマンスの問題の修正ではありませんが、何千ものアイテムをループする場合はおそらく遅延が避けられないことを示しており、長いKO操作の前に読み込みスピナーを表示してから非表示にすることができるパターンを使用していますその後それ。したがって、少なくとも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

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

IEで開発者ツールを開くと、この操作が大幅に遅くなります。配列に最大1000個の要素を追加しています。開発ツールを開いている場合、これには約10秒かかり、IEは実行中にフリーズします。開発ツールを閉じると、操作はすぐに実行され、IEで速度が低下することはありません。


0

また、IEではKnockout jsテンプレートエンジンの動作が遅いことに気付きました。underscore.jsに置き換えたところ、動作がはるかに速くなりました。


どうやってこれをしてくれましたか?
Stu Harper

@StuHarper私は、アンダースコアライブラリをインポートして、main.jsに私は、手順はアンダースコアの統合セクションで説明続いknockoutjs.com/documentation/template-binding.html
マルチェロ・

この改善はどのバージョンのIEで発生しましたか?
bkwdesign 2015

私はIE 10、11使用していた@bkwdesign
マルチェロ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.