Knockout.js sangat lambat di bawah set data semi-besar

86

Saya baru saja memulai dengan Knockout.js (selalu ingin mencobanya, tetapi sekarang saya akhirnya punya alasan!) - Namun, saya mengalami beberapa masalah kinerja yang sangat buruk saat mengikat tabel ke kumpulan yang relatif kecil. data (sekitar 400 baris atau lebih).

Dalam model saya, saya memiliki kode berikut:

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

Masalahnya adalah forloop di atas membutuhkan waktu sekitar 30 detik atau lebih dengan sekitar 400 baris. Namun, jika saya mengubah kodenya menjadi:

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

Kemudian forloop selesai dalam sekejap mata. Dengan kata lain, pushmetode objek Knockout observableArraysangat lambat.

Ini template saya:

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

Pertanyaan saya:

  1. Apakah ini cara yang tepat untuk mengikat data saya (yang berasal dari metode AJAX) ke koleksi yang dapat diamati?
  2. Saya berharap pushmelakukan beberapa penghitungan ulang berat setiap kali saya menyebutnya, seperti mungkin membangun kembali objek DOM yang terikat. Adakah cara untuk menunda penarikan kembali ini, atau mungkin memasukkan semua item saya sekaligus?

Saya dapat menambahkan lebih banyak kode jika diperlukan, tetapi saya cukup yakin inilah yang relevan. Untuk sebagian besar saya hanya mengikuti tutorial Knockout dari situs.

MEMPERBARUI:

Sesuai saran di bawah ini, saya telah memperbarui kode saya:

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

Namun, this.projects()masih membutuhkan waktu sekitar 10 detik untuk 400 baris. Saya akui, saya tidak yakin seberapa cepat ini tanpa Knockout (hanya menambahkan baris melalui DOM), tetapi saya merasa itu akan jauh lebih cepat dari 10 detik.

UPDATE 2:

Berdasarkan saran lain di bawah ini, saya memberi jQuery.tmpl kesempatan (yang secara native didukung oleh KnockOut), dan mesin templating ini akan menarik sekitar 400 baris hanya dalam waktu 3 detik. Ini sepertinya pendekatan terbaik, singkat dari solusi yang akan secara dinamis memuat lebih banyak data saat Anda menggulir.

Mike Christensen
sumber
1
Apakah Anda menggunakan sistem knockout foreach binding atau template binding dengan foreach. Saya hanya ingin tahu apakah menggunakan template dan menyertakan jquery tmpl alih-alih mesin template asli dapat membuat perbedaan.
madcapnmckay
1
@MikeChristensen - Knockout memiliki mesin templat asli sendiri yang terkait dengan ikatan (foreach, with). Ini juga mendukung mesin template lain, yaitu jquery.tmpl. Baca di sini untuk lebih jelasnya. Saya belum melakukan pembandingan dengan mesin yang berbeda jadi tidak tahu apakah itu akan membantu. Membaca komentar Anda sebelumnya, di IE7 Anda mungkin kesulitan untuk mendapatkan kinerja yang Anda cari.
madcapnmckay
2
Mengingat kita baru saja mendapatkan IE7 beberapa bulan yang lalu, saya rasa IE9 akan diluncurkan sekitar musim panas 2019. Oh, kita semua juga menggunakan WinXP .. Blech.
Mike Christensen
1
ps, Alasan tampaknya lambat adalah Anda menambahkan 400 item ke array yang dapat diamati itu secara individual . Untuk setiap perubahan pada observable, tampilan harus dirender untuk apa pun yang bergantung pada larik itu. Untuk templat yang kompleks dan banyak item untuk ditambahkan, itu banyak overhead ketika Anda bisa saja memperbarui larik sekaligus dengan menyetelnya ke contoh yang berbeda. Setidaknya, penguraian ulang akan dilakukan sekali.
Jeff Mercado
1
Saya menemukan cara yang lebih cepat dan rapi (tidak ada yang keluar dari kotak). menggunakan valueHasMutatedmelakukannya. periksa jawabannya jika Anda punya waktu.
sangat keren

Jawaban:

16

Seperti yang disarankan di komentar.

Knockout memiliki mesin template asli sendiri yang terkait dengan binding (foreach, with). Ini juga mendukung mesin template lain, yaitu jquery.tmpl. Baca di sini untuk lebih jelasnya. Saya belum melakukan pembandingan dengan mesin yang berbeda jadi tidak tahu apakah itu akan membantu. Membaca komentar Anda sebelumnya, di IE7 Anda mungkin kesulitan untuk mendapatkan kinerja yang Anda cari.

Selain itu, KO mendukung mesin templating js, jika seseorang telah menulis adaptor untuk itu. Anda mungkin ingin mencoba yang lain di luar sana karena jquery tmpl akan digantikan oleh JsRender .

madcapnmckay
sumber
Saya mendapatkan kinerja yang jauh lebih baik jquery.tmpljadi saya akan menggunakannya. Saya mungkin akan menyelidiki mesin lain dan juga menulis mesin saya sendiri jika saya memiliki waktu ekstra. Terima kasih!
Mike Christensen
1
@MikeChristensen - apakah Anda masih menggunakan data-bindpernyataan dalam template jQuery Anda, atau apakah Anda menggunakan sintaks $ {code}?
ericb
@ericb - Dengan kode baru, saya menggunakan ${code}sintaks dan jauh lebih cepat. Saya juga telah mencoba untuk membuat Underscore.js bekerja, tetapi belum beruntung ( <% .. %>sintaksnya mengganggu ASP.NET), dan sepertinya belum ada dukungan JsRender.
Mike Christensen
1
@MikeChristensen - ok, maka ini masuk akal. Mesin templat asli KO belum tentu tidak efisien. Saat Anda menggunakan sintaks $ {code}, Anda tidak mendapatkan pengikatan data apa pun pada elemen tersebut (yang meningkatkan kinerja). Jadi, jika Anda mengubah properti a ResultRow, itu tidak akan memperbarui UI (Anda harus memperbarui projectsobservableArray yang akan memaksa rendering ulang tabel Anda). $ {} pasti bisa menguntungkan jika data Anda cukup banyak hanya untuk dibaca
ericb
4
Penujuman! jquery.tmpl tidak lagi dalam pengembangan
Alex Larzelere
50

Silakan lihat: Knockout.js Performance Gotcha # 2 - Memanipulasi observableArrays

Pola yang lebih baik adalah mendapatkan referensi ke larik yang mendasari, mendorongnya, lalu memanggil .valueHasMutated (). Sekarang, pelanggan kami hanya akan menerima satu notifikasi yang menunjukkan bahwa array telah berubah.

Jim G.
sumber
13

Gunakan pagination dengan KO selain menggunakan $ .map.

Saya memiliki masalah yang sama dengan kumpulan data besar 1400 catatan sampai saya menggunakan paging dengan sistem gugur. Menggunakan $.mapuntuk memuat catatan memang membuat perbedaan besar tetapi waktu render DOM masih mengerikan. Kemudian saya mencoba menggunakan pagination dan itu membuat pencahayaan dataset saya cepat dan juga lebih ramah pengguna. Ukuran halaman 50 membuat kumpulan data tidak terlalu berlebihan dan mengurangi jumlah elemen DOM secara dramatis.

Ini sangat mudah dilakukan dengan KO:

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

Tim Santeford
sumber
11

KnockoutJS memiliki beberapa tutorial bagus, terutama tentang memuat dan menyimpan data

Dalam kasus mereka, mereka menarik data menggunakan getJSON()yang sangat cepat. Dari contoh mereka:

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);
    });    
}
deltree
sumber
1
Jelas peningkatan besar, tetapi self.tasks(mappedTasks)membutuhkan waktu sekitar 10 detik untuk berjalan (dengan 400 baris). Saya merasa ini masih belum bisa diterima.
Mike Christensen
Saya setuju bahwa 10 detik tidak dapat diterima. Menggunakan knockoutjs, saya tidak yakin mana yang lebih baik dari peta, jadi saya akan menyukai pertanyaan ini dan mencari jawaban yang lebih baik.
deltree
1
Baik. Jawabannya pasti layak +1untuk menyederhanakan kode saya dan meningkatkan kecepatan secara dramatis. Mungkin seseorang memiliki penjelasan yang lebih detail tentang apa itu bottleneck.
Mike Christensen
9

Berikan KoGrid lihat. Ini dengan cerdas mengelola rendering baris Anda sehingga lebih berkinerja.

Jika Anda mencoba mengikat 400 baris ke tabel menggunakan foreachpengikatan, Anda akan kesulitan mendorong sebanyak itu melalui KO ke DOM.

KO melakukan beberapa hal yang sangat menarik menggunakan foreachpengikatan, sebagian besar merupakan operasi yang sangat baik, tetapi mereka mulai merusak kinerja seiring dengan bertambahnya ukuran array Anda.

Saya telah melalui jalan gelap yang panjang untuk mencoba mengikat kumpulan data besar ke tabel / kisi, dan Anda akhirnya perlu memecah / halaman data secara lokal.

KoGrid melakukan ini semua. Ini dibuat untuk hanya merender baris yang dapat dilihat pemirsa di halaman, dan kemudian memvirtualisasikan baris lain hingga diperlukan. Saya pikir Anda akan menemukan bahwa kinerja 400 item jauh lebih baik daripada yang Anda alami.

ericb
sumber
1
Ini tampaknya benar-benar rusak di IE7 (tidak ada sampel yang berfungsi), jika tidak, ini akan bagus!
Mike Christensen
Senang melihatnya - KoGrid masih dalam pengembangan aktif. Namun, apakah ini setidaknya menjawab pertanyaan Anda tentang kinerja?
ericb
1
Ya! Ini menegaskan kecurigaan asli saya bahwa mesin templat KO default cukup lambat. Jika Anda membutuhkan seseorang untuk memelihara kelinci KoGrid untuk Anda, dengan senang hati saya akan melakukannya. Kedengarannya persis seperti yang kita butuhkan!
Mike Christensen
Menisik. Ini terlihat sangat bagus! Sayangnya, lebih dari 50% pengguna aplikasi saya menggunakan IE7!
Jim G.
Menariknya, saat ini kita harus ogah-ogahan mendukung IE11. Hal-hal telah membaik dalam 7 tahun terakhir.
MrBoJangles
5

Solusi untuk menghindari penguncian browser saat merender array yang sangat besar adalah dengan 'membatasi' array sedemikian rupa sehingga hanya beberapa elemen yang ditambahkan pada satu waktu, dengan sleep di antaranya. Inilah fungsi yang akan melakukan hal itu:

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

Bergantung pada kasus penggunaan Anda, ini dapat menghasilkan peningkatan UX besar-besaran, karena pengguna mungkin hanya melihat kumpulan baris pertama sebelum harus menggulir.

teh_senaus
sumber
Saya suka solusi ini, tetapi daripada setTimeout setiap iterasi, saya sarankan hanya menjalankan setTimout setiap 20 atau lebih iterasi karena setiap waktu juga membutuhkan waktu terlalu lama untuk dimuat. Saya melihat bahwa Anda melakukan itu dengan +20, tetapi itu tidak jelas bagi saya pada pandangan pertama.
charlierlee
5

Mengambil keuntungan dari push () menerima argumen variabel memberikan kinerja terbaik dalam kasus saya. 1300 baris dimuat selama 5973ms (~ 6 detik). Dengan pengoptimalan ini, waktu muat turun menjadi 914ms (<1 detik).
Itu berarti peningkatan 84,7%!

Info selengkapnya di Mendorong item ke 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);
};
mitaka
sumber
4

Saya telah berurusan dengan data dalam jumlah besar yang masuk untuk saya valueHasMutatedbekerja seperti pesona.

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

Setelah memanggil (4)data array akan dimuat ke observableArray yang dibutuhkan this.projectssecara otomatis.

jika Anda punya waktu, lihat ini dan jika ada masalah beri tahu saya

Trik di sini: Dengan melakukan seperti ini, jika dalam kasus dependensi (dihitung, berlangganan dll) dapat dihindari pada level push dan kita dapat membuatnya dieksekusi sekaligus setelah dipanggil (4).

sangat keren
sumber
1
Masalahnya tidak terlalu banyak panggilan ke push, masalahnya adalah bahwa bahkan satu panggilan untuk mendorong akan menyebabkan waktu render yang lama. Jika sebuah array memiliki 1000 item yang terikat ke a foreach, mendorong satu item merender keseluruhan foreach dan Anda membayar biaya waktu render yang besar.
Sedikit
1

Solusi yang mungkin, dalam kombinasi dengan menggunakan jQuery.tmpl, adalah dengan mendorong item pada suatu waktu ke array yang dapat diamati secara asinkron, menggunakan 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
  }
}

Dengan cara ini, saat Anda hanya menambahkan satu item dalam satu waktu, browser / knockout.js dapat menggunakan waktu untuk memanipulasi DOM sesuai, tanpa browser diblokir sepenuhnya selama beberapa detik, sehingga pengguna dapat menggulir daftar secara bersamaan.

gnab
sumber
2
Ini akan memaksa N sejumlah pembaruan DOM yang akan menghasilkan total waktu rendering yang jauh lebih lama daripada melakukan semuanya sekaligus.
Fredrik C
Itu tentu saja benar. Namun, intinya adalah bahwa kombinasi N menjadi angka besar dan mendorong item ke dalam larik proyek yang memicu sejumlah besar pembaruan atau penghitungan DOM lainnya, dapat menyebabkan browser membeku dan menawarkan Anda untuk menutup tab. Dengan memiliki waktu tunggu, baik per item atau per 10, 100 atau sejumlah item lainnya, browser akan tetap responsif.
gnab
2
Saya akan mengatakan bahwa ini adalah pendekatan yang salah dalam kasus umum di mana pembaruan total tidak akan membekukan browser tetapi itu adalah sesuatu untuk digunakan ketika semua yang lain gagal. Bagi saya ini terdengar seperti aplikasi yang ditulis dengan buruk di mana masalah kinerja harus diselesaikan, bukan hanya membuatnya tidak macet.
Fredrik C
1
Tentu saja ini adalah pendekatan yang salah dalam kasus umum, tidak ada yang akan setuju dengan Anda dalam hal itu. Ini adalah hack dan bukti konsep untuk mencegah browser freeze jika Anda perlu melakukan banyak operasi DOM. Saya membutuhkannya beberapa tahun yang lalu ketika membuat daftar beberapa tabel HTML besar dengan beberapa binding per sel, menghasilkan ribuan binding yang dievaluasi, masing-masing memengaruhi status DOM. Fungsionalitas tersebut diperlukan sementara, untuk memverifikasi kebenaran implementasi ulang aplikasi desktop berbasis Excel sebagai aplikasi web. Kemudian solusi ini berhasil dengan sempurna.
gnab
Komentar tersebut sebagian besar untuk dibaca orang lain untuk tidak berasumsi bahwa ini adalah cara yang disukai. Saya berasumsi bahwa Anda tahu apa yang Anda lakukan.
Fredrik C
1

Saya telah bereksperimen dengan kinerja, dan memiliki dua kontribusi yang saya harap dapat berguna.

Eksperimen saya fokus pada waktu manipulasi DOM. Jadi sebelum masuk ke ini, ada baiknya mengikuti poin-poin di atas tentang mendorong ke dalam array JS sebelum membuat array yang dapat diamati, dll.

Tetapi jika waktu manipulasi DOM masih menghalangi Anda, ini mungkin membantu:


1: Pola untuk membungkus spinner pemuatan di sekitar rendering lambat, lalu menyembunyikannya menggunakan afterRender

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

Ini sebenarnya bukan perbaikan untuk masalah kinerja, tetapi menunjukkan bahwa penundaan mungkin tidak terhindarkan jika Anda mengulang ribuan item dan ini menggunakan pola di mana Anda dapat memastikan Anda memiliki pemintal pemuatan muncul sebelum operasi KO yang lama, lalu sembunyikan itu sesudahnya. Jadi setidaknya itu meningkatkan UX.

Pastikan Anda dapat memuat 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)

Sembunyikan spinner:

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

yang memicu:

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

2: Menggunakan pengikatan html sebagai peretasan

Saya teringat teknik lama saat saya mengerjakan set top box dengan Opera, membuat UI menggunakan manipulasi DOM. Itu sangat lambat, jadi solusinya adalah menyimpan potongan besar HTML sebagai string, dan memuat string dengan menyetel properti innerHTML.

Hal serupa dapat dicapai dengan menggunakan pengikatan html dan penghitungan yang menurunkan HTML untuk tabel sebagai potongan besar teks, lalu menerapkannya sekaligus. Ini memperbaiki masalah kinerja, tetapi kerugian besar adalah sangat membatasi apa yang dapat Anda lakukan dengan mengikat di dalam setiap baris tabel.

Berikut biola yang menunjukkan pendekatan ini, bersama dengan fungsi yang bisa dipanggil dari dalam baris tabel untuk menghapus item dengan cara yang samar-samar seperti KO. Jelas ini tidak sebaik KO yang tepat, tetapi jika Anda benar-benar membutuhkan kinerja yang luar biasa, ini adalah solusi yang mungkin.

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

sifriday
sumber
1

Jika menggunakan IE, coba tutup alat dev.

Membuka alat pengembang di IE secara signifikan memperlambat operasi ini. Saya menambahkan ~ 1000 elemen ke sebuah array. Saat alat pengembang terbuka, ini membutuhkan waktu sekitar 10 detik dan IE berhenti saat itu terjadi. Ketika saya menutup alat dev, operasinya instan dan saya tidak melihat ada yang melambat di IE.

Daftar Jon
sumber
0

Saya juga memperhatikan bahwa mesin template Knockout js bekerja lebih lambat di IE, saya menggantinya dengan underscore.js, bekerja lebih cepat.

Marcello
sumber
Bagaimana Anda melakukan ini?
Stu Harper
@StuHarper Saya mengimpor pustaka garis bawah dan kemudian di main.js saya mengikuti langkah-langkah yang dijelaskan menggarisbawahi bagian integrasi dari knockoutjs.com/documentation/template-binding.html
Marcello
Dengan versi IE manakah peningkatan ini terjadi?
bkwdesign
@bkwdesign Saya menggunakan IE 10, 11.
Marcello