Knockout.js ist unter halbgroßen Datensätzen unglaublich langsam

86

Ich fange gerade erst mit Knockout.js an (wollte es schon immer mal ausprobieren, aber jetzt habe ich endlich eine Ausrede!) - Ich habe jedoch einige wirklich schlechte Leistungsprobleme, wenn ich einen Tisch an einen relativ kleinen Satz binde Daten (ungefähr 400 Zeilen oder so).

In meinem Modell habe ich folgenden Code:

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

Das Problem ist, dass die forobige Schleife mit ungefähr 400 Zeilen ungefähr 30 Sekunden dauert. Wenn ich den Code jedoch in Folgendes ändere:

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

Dann forendet die Schleife im Handumdrehen. Mit anderen Worten, die pushMethode von Knockouts observableArrayObjekt ist unglaublich langsam.

Hier ist meine Vorlage:

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

Meine Fragen:

  1. Ist dies der richtige Weg, um meine Daten (die von einer AJAX-Methode stammen) an eine beobachtbare Sammlung zu binden?
  2. Ich pushgehe davon aus, dass jedes Mal, wenn ich es aufrufe, eine gründliche Neuberechnung durchgeführt wird, z. B. das Wiederherstellen gebundener DOM-Objekte. Gibt es eine Möglichkeit, diese Neuberechnung zu verzögern oder alle meine Artikel gleichzeitig einzuschieben?

Ich kann bei Bedarf weiteren Code hinzufügen, bin mir aber ziemlich sicher, dass dies relevant ist. Zum größten Teil habe ich nur Knockout-Tutorials von der Site verfolgt.

AKTUALISIEREN:

Gemäß den folgenden Hinweisen habe ich meinen Code aktualisiert:

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

Es this.projects()dauert jedoch immer noch ungefähr 10 Sekunden für 400 Zeilen. Ich gebe zu, ich bin mir nicht sicher, wie schnell dies ohne Knockout wäre (nur Zeilen durch das DOM hinzufügen), aber ich habe das Gefühl, es wäre viel schneller als 10 Sekunden.

UPDATE 2:

Gemäß den folgenden Ratschlägen habe ich jQuery.tmpl ausprobiert (was von KnockOut nativ unterstützt wird), und diese Template-Engine zeichnet in etwas mehr als 3 Sekunden etwa 400 Zeilen. Dies scheint der beste Ansatz zu sein, kurz vor einer Lösung, bei der beim Scrollen dynamisch mehr Daten geladen werden.

Mike Christensen
quelle
1
Verwenden Sie eine Knockout-Foreach-Bindung oder eine Template-Bindung mit Foreach. Ich frage mich nur, ob die Verwendung von Templates und das Einbeziehen von jquery tmpl anstelle der nativen Template-Engine einen Unterschied machen kann.
Madcapnmckay
1
@MikeChristensen - Knockout verfügt über eine eigene native Template-Engine, die den (foreach, with) -Bindungen zugeordnet ist. Es unterstützt auch andere Template-Engines, nämlich jquery.tmpl. Lesen Sie hier für weitere Details. Ich habe kein Benchmarking mit verschiedenen Motoren durchgeführt, weiß also nicht, ob es helfen wird. Wenn Sie Ihren vorherigen Kommentar lesen, haben Sie in IE7 möglicherweise Schwierigkeiten, die gewünschte Leistung zu erzielen.
Madcapnmckay
2
Wenn man bedenkt, dass wir IE7 erst vor ein paar Monaten bekommen haben, denke ich, dass IE9 im Sommer 2019 eingeführt wird. Oh, wir sind auch alle auf WinXP. Blech.
Mike Christensen
1
ps, Der Grund, warum es langsam erscheint, ist, dass Sie diesem beobachtbaren Array einzeln 400 Elemente hinzufügen . Bei jeder Änderung am Observable muss die Ansicht für alles neu gerendert werden, was von diesem Array abhängt. Wenn komplexe Vorlagen und viele Elemente hinzugefügt werden müssen, bedeutet dies einen hohen Aufwand, wenn Sie das Array auf einmal auf einmal aktualisiert haben könnten, indem Sie es auf eine andere Instanz setzen. Zumindest dann würde das erneute Rendern einmal durchgeführt werden.
Jeff Mercado
1
Ich habe einen Weg gefunden, der schneller und ordentlicher ist (nichts aus der Box). mit valueHasMutatedmacht es. Überprüfen Sie die Antwort, wenn Sie Zeit haben.
Super cool

Antworten:

16

Wie in den Kommentaren vorgeschlagen.

Knockout verfügt über eine eigene native Template-Engine, die den (foreach, with) -Bindungen zugeordnet ist. Es unterstützt auch andere Template-Engines, nämlich jquery.tmpl. Lesen Sie hier für weitere Details. Ich habe kein Benchmarking mit verschiedenen Motoren durchgeführt, weiß also nicht, ob es helfen wird. Wenn Sie Ihren vorherigen Kommentar lesen, haben Sie in IE7 möglicherweise Schwierigkeiten, die gewünschte Leistung zu erzielen.

Abgesehen davon unterstützt KO jede js-Template-Engine, wenn jemand den Adapter dafür geschrieben hat. Vielleicht möchten Sie andere ausprobieren, da jquery tmpl durch JsRender ersetzt werden soll .

madcapnmckay
quelle
Ich werde viel besser mit, jquery.tmplalso werde ich das nutzen. Ich könnte andere Motoren untersuchen und meine eigenen schreiben, wenn ich etwas mehr Zeit habe. Vielen Dank!
Mike Christensen
1
@MikeChristensen - Verwenden Sie noch data-bindAnweisungen in Ihrer jQuery-Vorlage oder verwenden Sie die Syntax $ {code}?
Ericb
@ericb - Mit dem neuen Code verwende ich ${code}Syntax und es ist viel schneller. Ich habe auch versucht, Underscore.js <% .. %>zum Laufen zu bringen, hatte aber noch kein Glück (die Syntax stört ASP.NET), und es scheint noch keine JsRender-Unterstützung zu geben.
Mike Christensen
1
@ MikeChristensen - ok, dann macht das Sinn. Die native Template-Engine von KO ist nicht unbedingt so ineffizient. Wenn Sie die Syntax $ {code} verwenden, erhalten Sie keine Datenbindung für diese Elemente (was die Leistung verbessert). Wenn Sie also eine Eigenschaft von a ändern ResultRow, wird die Benutzeroberfläche nicht aktualisiert (Sie müssen das projectsObservableArray aktualisieren, wodurch ein erneutes Rendern Ihrer Tabelle erzwungen wird). $ {} kann definitiv vorteilhaft sein, wenn Ihre Daten so gut wie schreibgeschützt sind
Ericb
4
Nekromantie! jquery.tmpl ist nicht mehr in der Entwicklung
Alex Larzelere
50

Siehe: Knockout.js Performance Gotcha # 2 - Manipulieren von ObservableArrays

Ein besseres Muster besteht darin, einen Verweis auf unser zugrunde liegendes Array abzurufen, darauf zu pushen und dann .valueHasMutated () aufzurufen. Jetzt erhalten unsere Abonnenten nur eine Benachrichtigung, dass sich das Array geändert hat.

Jim G.
quelle
13

Verwenden Sie die Paginierung mit KO zusätzlich zu $ ​​.map.

Ich hatte das gleiche Problem mit großen Datensätzen von 1400 Datensätzen, bis ich Paging mit Knockout verwendete. Die Verwendung $.mapzum Laden der Datensätze machte einen großen Unterschied, aber die DOM-Renderzeit war immer noch abscheulich. Dann habe ich versucht, die Paginierung zu verwenden, und dadurch wurde meine Datensatzbeleuchtung schnell und benutzerfreundlicher. Eine Seitengröße von 50 machte den Datensatz weniger überwältigend und reduzierte die Anzahl der DOM-Elemente dramatisch.

Mit KO ist das sehr einfach:

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

Tim Santeford
quelle
11

KnockoutJS bietet einige großartige Tutorials, insbesondere zum Laden und Speichern von Daten

In ihrem Fall ziehen sie Daten, getJSON()die extrem schnell sind. Aus ihrem Beispiel:

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
quelle
1
Auf jeden Fall eine große Verbesserung, aber die self.tasks(mappedTasks)Ausführung dauert ungefähr 10 Sekunden (mit 400 Zeilen). Ich denke, das ist immer noch nicht akzeptabel.
Mike Christensen
Ich stimme zu, dass 10 Sekunden nicht akzeptabel sind. Bei Verwendung von knockoutjs bin ich mir nicht sicher, was besser ist als eine Karte. Daher werde ich diese Frage favorisieren und nach einer besseren Antwort Ausschau halten.
Deltree
1
OK. Die Antwort verdient definitiv eine, +1um meinen Code zu vereinfachen und die Geschwindigkeit dramatisch zu erhöhen. Vielleicht hat jemand eine detailliertere Erklärung, was der Engpass ist.
Mike Christensen
9

Schauen Sie sich KoGrid an . Es verwaltet Ihr Zeilen-Rendering intelligent, sodass es leistungsfähiger ist.

Wenn Sie versuchen, 400 Zeilen mithilfe einer foreachBindung an eine Tabelle zu binden, werden Sie Probleme haben, so viel durch KO in das DOM zu übertragen.

KO macht einige sehr interessante Dinge mit der foreachBindung, von denen die meisten sehr gute Operationen sind, aber sie beginnen mit der Größe zusammenzufallen, wenn die Größe Ihres Arrays zunimmt.

Ich war auf dem langen, dunklen Weg, große Datenmengen an Tabellen / Gitter zu binden, und am Ende müssen Sie die Daten lokal aufteilen / seiten.

KoGrid macht das alles. Es wurde erstellt, um nur die Zeilen zu rendern, die der Betrachter auf der Seite sehen kann, und um dann die anderen Zeilen zu virtualisieren, bis sie benötigt werden. Ich denke, Sie werden feststellen, dass die Leistung bei 400 Artikeln viel besser ist als bei Ihnen.

ericb
quelle
1
Dies scheint auf IE7 völlig kaputt zu sein (keines der Beispiele funktioniert), sonst wäre das großartig!
Mike Christensen
Ich bin froh, das zu prüfen - KoGrid befindet sich noch in der aktiven Entwicklung. Beantwortet dies jedoch zumindest Ihre Frage zu Perf?
Ericb
1
Jep! Es bestätigt meinen ursprünglichen Verdacht, dass die Standard-KO-Template-Engine ziemlich langsam ist. Wenn Sie jemanden brauchen, der KoGrid für Sie schweinet, würde ich mich freuen. Klingt nach genau dem, was wir brauchen!
Mike Christensen
Verflixt. Das sieht wirklich gut aus! Leider verwenden über 50% der Benutzer meiner Anwendung IE7!
Jim G.
Interessant, heutzutage müssen wir IE11 nur ungern unterstützen. Die Dinge haben sich in den letzten sieben Jahren verbessert.
MrBoJangles
5

Eine Lösung, um zu vermeiden, dass der Browser beim Rendern eines sehr großen Arrays blockiert wird, besteht darin, das Array so zu drosseln, dass nur wenige Elemente gleichzeitig hinzugefügt werden, wobei dazwischen ein Ruhezustand liegt. Hier ist eine Funktion, die genau das tut:

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

Abhängig von Ihrem Anwendungsfall kann dies zu einer massiven UX-Verbesserung führen, da der Benutzer möglicherweise nur den ersten Zeilenstapel sieht, bevor er einen Bildlauf durchführen muss.

teh_senaus
quelle
Ich mag diese Lösung, aber anstatt setTimeout bei jeder Iteration festzulegen, empfehle ich, setTimout nur alle 20 oder mehr Iterationen auszuführen, da das Laden jedes Mal zu lange dauert. Ich sehe, dass Sie das mit der +20 machen, aber es war für mich auf den ersten Blick nicht offensichtlich.
charlierlee
5

Push () zu nutzen, um variable Argumente zu akzeptieren, ergab in meinem Fall die beste Leistung. 1300 Reihen wurden für 5973 ms (~ 6 s) geladen. Mit dieser Optimierung wurde die Ladezeit auf 914 ms (<1 Sek.) Reduziert.
Das ist eine Verbesserung von 84,7%!

Weitere Informationen finden Sie unter Verschieben von Elementen in ein beobachtbares Array

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

Ich hatte es mit so großen Datenmengen zu tun, dass es für mich valueHasMutatedwie ein Zauber war.

Modell anzeigen:

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) 

Nach dem Aufruf des (4)Arrays werden die Daten this.projectsautomatisch in das erforderliche ObservableArray geladen .

Wenn Sie Zeit haben, schauen Sie sich das an und lassen Sie es mich wissen, falls es Probleme gibt

Trick hier: Auf diese Weise können Abhängigkeiten (berechnet, abonniert usw.) auf Push-Ebene vermieden werden und nach dem Aufruf auf einmal ausgeführt werden (4).

Super cool
quelle
1
Das Problem sind nicht zu viele Aufrufe push, das Problem ist, dass selbst ein einzelner Aufruf zum Pushen lange Renderzeiten verursacht. Wenn in einem Array 1000 Elemente an a gebunden sind, foreachwird durch Drücken eines einzelnen Elements das gesamte Foreach erneut gerendert, und Sie zahlen hohe Kosten für die Renderzeit.
Leicht
1

Eine mögliche Problemumgehung in Kombination mit der Verwendung von jQuery.tmpl besteht darin, Elemente mithilfe von setTimeout asynchron auf das beobachtbare Array zu übertragen.

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

Auf diese Weise kann sich der Browser / knockout.js die Zeit nehmen, um das DOM entsprechend zu bearbeiten, wenn Sie jeweils nur ein einzelnes Element hinzufügen, ohne dass der Browser für einige Sekunden vollständig blockiert wird, sodass der Benutzer gleichzeitig durch die Liste scrollen kann.

gnab
quelle
2
Dies erzwingt N Anzahl von DOM-Aktualisierungen, was zu einer Gesamtrenderzeit führt, die viel länger ist, als alles auf einmal zu tun.
Fredrik C
Das ist natürlich richtig. Der Punkt ist jedoch, dass die Kombination von N als große Zahl und dem Verschieben eines Elements in das Projektarray, das eine erhebliche Anzahl anderer DOM-Aktualisierungen oder -Berechnungen auslöst, dazu führen kann, dass der Browser einfriert und Ihnen anbietet, die Registerkarte zu beenden. Durch eine Zeitüberschreitung, entweder pro Element oder pro 10, 100 oder einer anderen Anzahl von Elementen, reagiert der Browser weiterhin.
Gnab
2
Ich würde sagen, dass dies der falsche Ansatz im allgemeinen Fall ist, in dem das gesamte Update den Browser nicht einfrieren würde, aber es ist etwas zu verwenden, wenn alle anderen fehlschlagen. Für mich klingt es wie eine schlecht geschriebene Anwendung, bei der die Leistungsprobleme gelöst werden sollten, anstatt sie nur einfrieren zu lassen.
Fredrik C
1
Natürlich ist es im allgemeinen Fall der falsche Ansatz, niemand würde Ihnen darin widersprechen. Dies ist ein Hack und ein Proof-of-Concept, um das Einfrieren des Browsers zu verhindern, wenn Sie viele DOM-Vorgänge ausführen müssen. Ich brauchte es vor ein paar Jahren, als ich mehrere große HTML-Tabellen mit mehreren Bindungen pro Zelle auflistete, was dazu führte, dass Tausende von Bindungen ausgewertet wurden, die sich jeweils auf den Status des DOM auswirkten. Die Funktionalität wurde vorübergehend benötigt, um die Richtigkeit der Neuimplementierung einer Excel-basierten Desktopanwendung als Webanwendung zu überprüfen. Dann hat diese Lösung perfekt geklappt.
Gnab
Der Kommentar war hauptsächlich für andere zu lesen, um nicht anzunehmen, dass dies der bevorzugte Weg war. Ich nahm an, dass Sie wussten, was Sie taten.
Fredrik C
1

Ich habe mit Leistung experimentiert und habe zwei Beiträge, von denen ich hoffe, dass sie nützlich sind.

Meine Experimente konzentrieren sich auf die DOM-Manipulationszeit. Bevor Sie darauf eingehen, sollten Sie unbedingt die obigen Punkte zum Verschieben in ein JS-Array befolgen, bevor Sie ein beobachtbares Array usw. erstellen.

Wenn Ihnen die DOM-Manipulationszeit immer noch im Weg steht, kann dies helfen:


1: Ein Muster, mit dem ein Ladespinner um das langsame Rendern gewickelt und dann mit afterRender ausgeblendet wird

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

Dies ist keine wirkliche Lösung für das Leistungsproblem, zeigt jedoch, dass eine Verzögerung wahrscheinlich unvermeidlich ist, wenn Sie Tausende von Elementen durchlaufen und ein Muster verwenden, mit dem Sie sicherstellen können, dass vor dem langen KO-Vorgang ein Ladespinner angezeigt und dann ausgeblendet wird es danach. So verbessert es zumindest die UX.

Stellen Sie sicher, dass Sie einen Spinner laden können:

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

Verstecke den Spinner:

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

was auslöst:

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

2: Verwenden der HTML-Bindung als Hack

Ich erinnerte mich an eine alte Technik aus der Zeit, als ich mit Opera an einer Set-Top-Box arbeitete und die Benutzeroberfläche mithilfe der DOM-Manipulation erstellte. Es war entsetzlich langsam, daher bestand die Lösung darin, große HTML-Blöcke als Zeichenfolgen zu speichern und die Zeichenfolgen durch Festlegen der innerHTML-Eigenschaft zu laden.

Ähnliches kann erreicht werden, indem die HTML-Bindung und eine Berechnung verwendet werden, die den HTML-Code für die Tabelle als großen Textblock ableitet und ihn dann auf einmal anwendet. Dies behebt zwar das Leistungsproblem, aber der massive Nachteil besteht darin, dass die Möglichkeiten zum Binden in jeder Tabellenzeile stark eingeschränkt werden.

Hier ist eine Geige, die diesen Ansatz zeigt, zusammen mit einer Funktion, die aus den Tabellenzeilen heraus aufgerufen werden kann, um ein Element auf vage KO-ähnliche Weise zu löschen. Natürlich ist dies nicht so gut wie ein richtiger KO, aber wenn Sie wirklich eine hervorragende Leistung benötigen, ist dies eine mögliche Problemumgehung.

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

Freitag
quelle
1

Wenn Sie den IE verwenden, schließen Sie die Entwicklertools.

Wenn die Entwicklertools im IE geöffnet sind, wird dieser Vorgang erheblich verlangsamt. Ich füge einem Array ~ 1000 Elemente hinzu. Wenn die Entwicklungswerkzeuge geöffnet sind, dauert dies ungefähr 10 Sekunden und der IE friert ein, während dies geschieht. Wenn ich die Entwickler-Tools schließe, ist der Vorgang sofort und ich sehe keine Verlangsamung im IE.

Jon List
quelle
0

Mir ist auch aufgefallen, dass die Knockout js Template Engine im IE langsamer arbeitet. Ich habe sie durch underscore.js ersetzt und arbeite viel schneller.

Marcello
quelle
Wie hast du das bitte gemacht?
Stu Harper
@StuHarper Ich habe die Unterstrichbibliothek importiert und dann in main.js die Schritte befolgt, die im Abschnitt zur Unterstrichintegration von knockoutjs.com/documentation/template-binding.html
Marcello
Mit welcher IE-Version ist diese Verbesserung aufgetreten?
bkwdesign
@bkwdesign Ich benutzte IE 10, 11.
Marcello