Knockout.js è incredibilmente lento con set di dati semi-grandi


86

Sto appena iniziando con Knockout.js (ho sempre voluto provarlo, ma ora ho finalmente una scusa!) - Tuttavia, sto riscontrando dei problemi di prestazioni davvero brutti quando associo una tabella a un insieme relativamente piccolo di dati (circa 400 righe circa).

Nel mio modello ho il seguente codice:

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

Il problema è che il forciclo sopra richiede circa 30 secondi circa con circa 400 righe. Tuttavia, se cambio il codice in:

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

Quindi il forciclo si completa in un batter d'occhio. In altre parole, il pushmetodo observableArraydell'oggetto Knockout è incredibilmente lento.

Ecco il mio modello:

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

Le mie domande:

  1. È questo il modo giusto per associare i miei dati (che provengono da un metodo AJAX) a una raccolta osservabile?
  2. Mi aspetto che pushesegua un pesante ricalcolo ogni volta che lo chiamo, come forse la ricostruzione di oggetti DOM associati. C'è un modo per ritardare questo ricalcolo o forse inserire tutti i miei elementi contemporaneamente?

Posso aggiungere altro codice se necessario, ma sono abbastanza sicuro che questo sia ciò che è rilevante. Per la maggior parte stavo solo seguendo i tutorial di Knockout dal sito.

AGGIORNARE:

Per i consigli seguenti, ho aggiornato il mio codice:

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

Tuttavia, this.projects()occorrono ancora circa 10 secondi per 400 righe. Ammetto di non essere sicuro di quanto sarebbe veloce senza Knockout (aggiungendo solo righe attraverso il DOM), ma ho la sensazione che sarebbe molto più veloce di 10 secondi.

AGGIORNAMENTO 2:

Per altri consigli di seguito, ho dato una possibilità a jQuery.tmpl (che è supportato nativamente da KnockOut) e questo motore di modelli disegnerà circa 400 righe in poco più di 3 secondi. Questo sembra l'approccio migliore, a parte una soluzione che caricherà dinamicamente più dati durante lo scorrimento.


1
Stai usando un knockout foreach binding o il template binding con foreach. Mi chiedo solo se l'utilizzo di template e l'inclusione di jquery tmpl al posto del template engine nativo possa fare la differenza.
madcapnmckay

1
@MikeChristensen - Knockout ha il proprio motore di template nativo associato alle associazioni (foreach, with). Supporta anche altri motori di template, vale a dire jquery.tmpl. Leggi qui per maggiori dettagli. Non ho fatto alcun benchmarking con motori diversi, quindi non so se aiuterà. Leggendo il tuo commento precedente, in IE7 potresti avere difficoltà a ottenere le prestazioni che stai cercando.
madcapnmckay

2
Considerando che abbiamo appena ricevuto IE7 pochi mesi fa, penso che IE9 uscirà intorno all'estate 2019. Oh, anche noi siamo tutti su WinXP .. Blech.
Mike Christensen

1
ps, il motivo per cui sembra lento è che stai aggiungendo 400 elementi a quell'array osservabile individualmente . Per ogni modifica all'osservabile, la visualizzazione deve essere riesaminata per tutto ciò che dipende da quell'array. Per modelli complessi e molti elementi da aggiungere, è molto sovraccarico quando avresti potuto aggiornare l'array tutto in una volta impostandolo su un'istanza diversa. Almeno allora, il rendering verrà eseguito una volta.
Jeff Mercado,

1
Ho trovato un modo che è più veloce e pulito (niente fuori dagli schemi). usando lo valueHasMutatedfa. controlla la risposta se hai tempo.
super cool

Risposte:


16

Come suggerito nei commenti.

Knockout ha il proprio motore di template nativo associato alle associazioni (foreach, with). Supporta anche altri motori di template, vale a dire jquery.tmpl. Leggi qui per maggiori dettagli. Non ho fatto alcun benchmarking con motori diversi, quindi non so se aiuterà. Leggendo il tuo commento precedente, in IE7 potresti avere difficoltà a ottenere le prestazioni che stai cercando.

Per inciso, KO supporta qualsiasi motore di templating js, se qualcuno ha scritto l'adattatore per questo. Potresti provare altri là fuori poiché jquery tmpl dovrebbe essere sostituito da JsRender .


Sto ottenendo prestazioni molto migliori con jquery.tmplquindi lo userò. Potrei indagare su altri motori e scrivere il mio se ho un po 'di tempo in più. Grazie!
Mike Christensen

1
@MikeChristensen - stai ancora utilizzando data-binddichiarazioni nel tuo modello jQuery o stai usando la sintassi $ {code}?
ericb

@ericb - Con il nuovo codice, sto usando la ${code}sintassi ed è molto più veloce. Ho anche cercato di far funzionare Underscore.js, ma non ho ancora avuto fortuna (la <% .. %>sintassi interferisce con ASP.NET) e sembra che non ci sia ancora il supporto JsRender.
Mike Christensen

1
@MikeChristensen - ok, allora ha senso. Il motore di modelli nativi di KO non è necessariamente così inefficiente. Quando si utilizza la sintassi $ {code}, non si ottiene alcun binding di dati su quegli elementi (il che migliora le prestazioni). Pertanto, se modifichi una proprietà di a ResultRow, non aggiornerà l'interfaccia utente (dovrai aggiornare l' projectsosservableArray che forzerà un nuovo rendering della tua tabella). $ {} può sicuramente essere vantaggioso se i tuoi dati sono praticamente di sola lettura
ericb

4
Negromanzia! jquery.tmpl non è più in sviluppo
Alex Larzelere


13

Usa l'impaginazione con KO oltre a usare $ .map.

Ho avuto lo stesso problema con un set di dati di grandi dimensioni di 1400 record fino a quando non ho utilizzato il paging con knockout. Usare $.mapper caricare i record ha fatto un'enorme differenza, ma il tempo di rendering del DOM era ancora orribile. Poi ho provato a usare l'impaginazione e questo ha reso l'illuminazione del set di dati più veloce e più facile da usare. Una dimensione pagina di 50 ha reso il set di dati molto meno opprimente e ha ridotto drasticamente il numero di elementi DOM.

È molto facile da fare con KO:

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


11

KnockoutJS ha alcuni ottimi tutorial, in particolare quello sul caricamento e il salvataggio dei dati

Nel loro caso, estraggono i dati utilizzando il getJSON()che è estremamente veloce. Dal loro esempio:

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
Sicuramente un grande miglioramento, ma ci self.tasks(mappedTasks)vogliono circa 10 secondi per funzionare (con 400 righe). Penso che questo non sia ancora accettabile.
Mike Christensen

Sono d'accordo che 10 secondi non sono accettabili. Utilizzando knockoutjs, non sono sicuro di cosa sia meglio di una mappa, quindi preferirò questa domanda e cercherò una risposta migliore.
deltree

1
Ok. La risposta merita sicuramente una +1per semplificare il mio codice e aumentare notevolmente la velocità. Forse qualcuno ha una spiegazione più dettagliata di cosa sia il collo di bottiglia.
Mike Christensen

9

Dai un'occhiata a KoGrid . Gestisce in modo intelligente il rendering delle righe in modo che sia più performante.

Se stai cercando di associare 400 righe a una tabella utilizzando un'associazione foreach, avrai problemi a spingere così tanto attraverso KO nel DOM.

KO fa alcune cose molto interessanti usando il foreachbinding, la maggior parte delle quali sono operazioni molto buone, ma iniziano a rompersi alla perfezione man mano che la dimensione del tuo array cresce.

Ho seguito la lunga e oscura strada del tentativo di legare grandi set di dati a tabelle / griglie e alla fine hai bisogno di separare / pagina i dati localmente.

KoGrid fa tutto questo. È stato creato per eseguire il rendering solo delle righe che il visualizzatore può vedere sulla pagina e quindi virtualizzare le altre righe finché non sono necessarie. Penso che troverai la sua perfetta su 400 articoli molto meglio di quanto stai vivendo.


1
Questo sembra essere completamente rotto su IE7 (nessuno dei campioni funziona), altrimenti sarebbe fantastico!
Mike Christensen

Sono contento di esaminarlo: KoGrid è ancora in fase di sviluppo attivo. Tuttavia, questo almeno risponde alla tua domanda riguardo alla perf?
ericb

1
Sì! Conferma il mio sospetto originale che il motore di modelli KO predefinito sia piuttosto lento. Se hai bisogno di qualcuno che ti aiuti a cavare KoGrid, ne sarei felice. Sembra esattamente quello che ci serve!
Mike Christensen

Dannazione. Questo sembra davvero buono! Sfortunatamente, oltre il 50% degli utenti della mia applicazione utilizza IE7!
Jim G.

Interessante, oggigiorno dobbiamo sostenere con riluttanza IE11. Le cose sono migliorate negli ultimi 7 anni.
MrBoJangles

5

Una soluzione per evitare di bloccare il browser quando si esegue il rendering di un array molto grande è di "accelerare" l'array in modo tale che vengano aggiunti solo pochi elementi alla volta, con uno sleep nel mezzo. Ecco una funzione che farà proprio questo:

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

A seconda del tuo caso d'uso, ciò potrebbe comportare un enorme miglioramento della UX, poiché l'utente potrebbe vedere solo il primo batch di righe prima di dover scorrere.


Mi piace questa soluzione, ma piuttosto che setTimeout ogni iterazione, consiglio di eseguire setTimout ogni 20 o più iterazioni perché ogni volta richiede anche troppo tempo per il caricamento. Vedo che lo stai facendo con il +20, ma a prima vista non era ovvio.
charlierlee

5

Approfittando di push () che accetta argomenti variabili ha dato le migliori prestazioni nel mio caso. 1300 righe venivano caricate per 5973 ms (~ 6 sec.). Con questa ottimizzazione il tempo di caricamento è sceso a 914 ms (<1 sec.)
, Con un miglioramento dell'84,7%!

Altre informazioni in 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);
};

4

Ho avuto a che fare con volumi così enormi di dati in arrivo per me ha valueHasMutatedfunzionato a meraviglia.

Visualizza modello:

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) 

Dopo aver chiamato l' (4)array, i dati verranno caricati nell'osservableArray richiesto, che viene this.projectsautomaticamente.

se hai tempo dai un'occhiata a questo e in caso di problemi fammelo sapere

Trucco qui: in questo modo, se in caso di dipendenze (calcolate, sottoscrizioni ecc.) Possono essere evitate a livello di push e possiamo farle eseguire in una volta dopo la chiamata (4).


1
Il problema non sono troppe chiamate a push, il problema è che anche una singola chiamata da inviare causerà lunghi tempi di rendering. Se un array ha 1000 elementi associati a un foreach, il push di un singolo elemento restituisce l'intero foreach e si paga un elevato tempo di rendering.
Leggero

1

Una possibile soluzione, in combinazione con l'utilizzo di jQuery.tmpl, è di inserire elementi alla volta nell'array osservabile in modo asincrono, utilizzando 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
  }
}

In questo modo, quando aggiungi un solo elemento alla volta, il browser / knockout.js può impiegare del tempo per manipolare il DOM di conseguenza, senza che il browser venga completamente bloccato per diversi secondi, in modo che l'utente possa scorrere l'elenco contemporaneamente.


2
Ciò forzerà il numero N di aggiornamenti DOM che si tradurrà in un tempo di rendering totale che è molto più lungo rispetto a fare tutto in una volta.
Fredrik C

Ovviamente è corretto. Il punto è, tuttavia, che la combinazione di N che è un numero elevato e che inserisce un elemento nell'array dei progetti attivando una quantità significativa di altri aggiornamenti o calcoli DOM, potrebbe causare il blocco del browser e offrirti di chiudere la scheda. Avendo un timeout, per elemento o per 10, 100 o un altro numero di elementi, il browser sarà comunque reattivo.
gnab

2
Direi che questo è l'approccio sbagliato nel caso generale in cui l'aggiornamento totale non congelerebbe il browser ma è qualcosa da usare quando tutti gli altri falliscono. Per me suona come un'applicazione scritta male in cui i problemi di prestazioni dovrebbero essere risolti invece di limitarsi a non bloccarli.
Fredrik C

1
Ovviamente è l'approccio sbagliato nel caso generale, nessuno sarebbe in disaccordo con te in questo. Questo è un trucco e una prova di concetto per prevenire il blocco del browser se devi eseguire un sacco di operazioni DOM. Ne avevo bisogno un paio di anni fa quando elencavo diverse tabelle HTML di grandi dimensioni con diversi binding per cella, con il risultato di valutare migliaia di binding, ciascuno dei quali influiva sullo stato del DOM. La funzionalità era temporaneamente necessaria per verificare la correttezza della reimplementazione di un'applicazione desktop basata su Excel come applicazione web. Quindi questa soluzione ha funzionato perfettamente.
gnab

Il commento doveva essere letto principalmente da altri per non dare per scontato che questo fosse il modo preferito. Pensavo che sapessi cosa stavi facendo.
Fredrik C

1

Ho sperimentato le prestazioni e ho due contributi che spero possano essere utili.

I miei esperimenti si concentrano sul tempo di manipolazione del DOM. Quindi, prima di entrare in questo, vale sicuramente la pena seguire i punti precedenti sull'inserimento in un array JS prima di creare un array osservabile, ecc.

Ma se il tempo di manipolazione del DOM ti sta ancora intralciando, allora questo potrebbe aiutare:


1: un modello per avvolgere una casella di selezione di caricamento attorno al rendering lento, quindi nasconderlo utilizzando afterRender

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

Questa non è davvero una soluzione per il problema delle prestazioni, ma mostra che un ritardo è probabilmente inevitabile se esegui il loop su migliaia di elementi e utilizza uno schema in cui puoi assicurarti di avere uno spinner di caricamento prima della lunga operazione KO, quindi nascondi in seguito. Quindi migliora l'UX, almeno.

Assicurati di poter caricare uno 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)

Nascondi lo spinner:

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

che innesca:

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

2: utilizzo dell'associazione html come hack

Ho ricordato una vecchia tecnica di quando stavo lavorando su un set top box con Opera, costruendo l'interfaccia utente utilizzando la manipolazione DOM. Era terribilmente lento, quindi la soluzione era memorizzare grandi blocchi di HTML come stringhe e caricare le stringhe impostando la proprietà innerHTML.

Qualcosa di simile può essere ottenuto utilizzando l'associazione html e un computer che ricava l'HTML per la tabella come un grosso pezzo di testo, quindi lo applica in una volta. Questo risolve il problema delle prestazioni, ma l'enorme svantaggio è che limita fortemente ciò che puoi fare con l'associazione all'interno di ogni riga della tabella.

Ecco un violino che mostra questo approccio, insieme a una funzione che può essere chiamata dall'interno delle righe della tabella per eliminare un elemento in un modo vagamente KO. Ovviamente questo non è buono come il KO corretto, ma se hai davvero bisogno di prestazioni eccezionali, questa è una possibile soluzione alternativa.

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


1

Se usi IE, prova a chiudere gli strumenti di sviluppo.

Avere gli strumenti per sviluppatori aperti in IE rallenta notevolmente questa operazione. Sto aggiungendo ~ 1000 elementi a un array. Quando gli strumenti di sviluppo sono aperti, ci vogliono circa 10 secondi e IE si blocca mentre sta accadendo. Quando chiudo gli strumenti di sviluppo, l'operazione è istantanea e non vedo alcun rallentamento in IE.


0

Ho anche notato che il motore di template Knockout js funziona più lentamente in IE, l'ho sostituito con underscore.js, funziona molto più velocemente.


Come hai fatto per favore?
Stu Harper

@StuHarper ho importato la libreria underscore e poi in main.js ho seguito i passaggi descritti nella sezione underscore integration di knockoutjs.com/documentation/template-binding.html
Marcello

Con quale versione di IE si è verificato questo miglioramento?
bkwdesign

@bkwdesign stavo usando IE 10, 11.
Marcello
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.