web-dev-qa-db-fra.com

Knockout.js incroyablement lent sous des ensembles de données semi-volumineux

Je viens juste de commencer à utiliser Knockout.js (je voulais toujours l'essayer, mais maintenant, j'ai enfin une excuse!) - Cependant, je rencontre de très mauvais problèmes de performances lorsque je lie une table à un ensemble relativement petit. données (environ 400 lignes ou plus).

Dans mon modèle, j'ai le code suivant:

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

Le problème est que la boucle for ci-dessus prend environ 30 secondes avec environ 400 lignes. Cependant, si je change le code en:

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

Ensuite, la boucle for se termine en un clin d'œil. En d'autres termes, la méthode Push de l'objet observableArray de Knockout est incroyablement lente.

Voici mon modèle:

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

Mes questions:

  1. Est-ce la bonne façon de lier mes données (qui proviennent d'une méthode AJAX) à une collection observable?
  2. Je m'attends à ce que Push répète chaque fois que je l'appelle, par exemple en reconstruisant des objets DOM liés. Existe-t-il un moyen de retarder ce calcul, ou peut-être d'insérer tous mes objets en même temps?

Je peux ajouter plus de code si nécessaire, mais je suis à peu près sûr que c'est ce qui est pertinent. Pour la plupart, je ne faisais que suivre les didacticiels Knockout du site.

METTRE À JOUR:

Selon les conseils ci-dessous, j'ai mis à jour mon code:

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

Cependant, this.projects() prend encore environ 10 secondes pour 400 lignes. J'admets que je ne suis pas sûr de la rapidité avec sans Knockout (il suffit d'ajouter des rangées dans le DOM), mais j'ai l'impression que ce serait beaucoup plus rapide que 10 secondes.

MISE À JOUR 2:

Selon les conseils ci-dessous, j’ai donné jQuery.tmpl a shot (qui est supporté de manière native par KnockOut), et ce moteur de gabarit dessinera environ 400 lignes en un peu plus de 3 secondes. Cela semble être la meilleure approche, à moins d'une solution qui chargerait dynamiquement plus de données à mesure que vous faites défiler.

83
Mike Christensen

Comme suggéré dans les commentaires.

Knockout a son propre moteur de template natif associé aux liaisons (foreach, with). Il prend également en charge d'autres moteurs de template, à savoir jquery.tmpl. Lisez ici pour plus de détails. Je n'ai pas fait d'analyse comparative avec différents moteurs, alors je ne sais pas si cela aidera. En lisant votre commentaire précédent, dans IE7, vous aurez peut-être du mal à obtenir la performance que vous recherchez.

En passant, KO supporte tous les moteurs de templates js, si quelqu'un a écrit l'adaptateur pour cela. Vous voudrez peut-être en essayer d'autres car jquery tmpl doit être remplacé par JsRender .

16
madcapnmckay

S'il vous plaît voir: Knockout.js Performance Gotcha # 2 - Manipulating observableArrays

Un meilleur modèle consiste à obtenir une référence à notre tableau sous-jacent, Push to it, puis appelez .valueHasMutated (). Désormais, nos abonnés ne recevront qu'une notification indiquant que le tableau a été modifié.

50
Jim G.

Utilisez la pagination avec KO en plus de $ .map.

J'ai eu le même problème avec un grand jeu de données de 1400 enregistrements jusqu'à ce que j'utilise la pagination avec knockout. Utiliser $.map pour charger les enregistrements faisait une énorme différence, mais le temps de rendu du DOM était toujours hideux. J'ai ensuite essayé d'utiliser la pagination, ce qui a rendu l'éclairage de mon jeu de données rapide et convivial. Une taille de page de 50 rend le jeu de données beaucoup moins contraignant et réduit considérablement le nombre d'éléments DOM.

C'est très facile à faire avec KO:

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

13
Tim Santeford

KnockoutJS propose de très bons tutoriels, en particulier celui sur le chargement et la sauvegarde de données

Dans leur cas, ils extraient les données en utilisant getJSON() qui est extrêmement rapide. De leur exemple:

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

Donnez KoGrid un coup d'oeil. Il gère intelligemment votre rendu de ligne afin qu'il soit plus performant.

Si vous essayez de lier 400 lignes à une table en utilisant une liaison foreach, vous aurez du mal à insérer autant dans KO dans DOM.

KO fait des choses très intéressantes en utilisant la liaison foreach, dont la plupart sont de très bonnes opérations, mais elles commencent à se détériorer à mesure que la taille de votre tableau augmente.

J'ai essayé de lier des ensembles de données volumineux à des tableaux/grilles, et vous devez donc séparer/paginer les données localement.

KoGrid fait tout cela. Il a été conçu pour afficher uniquement les lignes visibles par le visualiseur sur la page, puis virtualiser les autres lignes jusqu'à leur utilisation. Je pense que vous constaterez que ses performances sur 400 articles sont bien meilleures que celles que vous vivez.

9
ericb

Une solution pour éviter de verrouiller le navigateur lors du rendu d'un tableau très volumineux consiste à «l'étrangler» de manière à ce que seuls quelques éléments soient ajoutés à la fois, avec une veille entre les deux. Voici une fonction qui fera exactement cela:

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

En fonction de votre cas d'utilisation, cela pourrait entraîner une amélioration considérable de l'UX, l'utilisateur pouvant uniquement voir le premier lot de lignes avant de devoir faire défiler.

4
teh_senaus

Tirer parti de Push () en acceptant des arguments variables a donné les meilleures performances dans mon cas . Avec cette optimisation, le temps de chargement était réduit à 914ms (<1 sec.)
Cela représente une amélioration de 84,7%!

Plus d'infos sur Ajout d'éléments à un 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
mitaka

J'ai eu affaire à d'énormes volumes de données entrant pour moi valueHasMutated a fonctionné à merveille.

Voir le modèle:

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) 

Après avoir appelé (4), les données du tableau seront chargées dans l'observableArray requis, qui est automatiquement this.projects.

si vous avez le temps, jetez un coup d'oeil à cela et juste au cas où vous auriez du mal à me le faire savoir 

Astuce ici: En procédant comme ceci, si dans le cas de dépendances (calculées, abonnées etc.) peuvent être évitées au niveau Push et nous pouvons les faire exécuter en une fois après avoir appelé (4).

4
super cool

Une solution possible, associée à l'utilisation de jQuery.tmpl, consiste à envoyer des éléments à la fois au tableau observable de manière asynchrone, à l'aide de 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
  }
}

Ainsi, lorsque vous ajoutez un seul élément à la fois, le fichier browser/knockout.js peut prendre son temps pour manipuler le DOM, sans que le navigateur ne soit complètement bloqué pendant plusieurs secondes, afin que l'utilisateur puisse faire défiler la liste simultanément.

1
gnab

J'ai expérimenté la performance et j'ai deux contributions qui, je l'espère, pourraient être utiles.

Mes expériences portent sur le temps de manipulation du DOM. Donc, avant d’entrer dans cette discussion, il vaut vraiment la peine de suivre les points ci-dessus concernant le transfert dans un tableau JS avant de créer un tableau observable, etc.

Mais si le temps de manipulation du DOM vous gêne, cela pourrait aider:


1: Un motif pour envelopper une spinner de chargement autour du rendu lent, puis le cacher en utilisant afterRender

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

Ce n'est pas vraiment une solution au problème de performances, mais cela montre qu'un délai est probablement inévitable si vous parcourez des milliers d'articles et qu'il utilise un modèle vous permettant de vous assurer qu'une spinner de chargement apparaisse avant la longue opération KO, puis de la masquer. il après. Donc, cela améliore au moins l'expérience utilisateur.

Assurez-vous de pouvoir charger une casserole:

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

Masquer le spinner:

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

qui déclenche:

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

2: Utiliser la liaison HTML comme un hack

Je me suis souvenu d'une technique ancienne de l'époque où je travaillais sur un décodeur avec Opera, construisant une interface utilisateur à l'aide de la manipulation DOM. Elle était effroyablement lente. La solution consistait donc à stocker de gros morceaux de code HTML sous forme de chaînes et à charger les chaînes en définissant la propriété innerHTML. 

Vous pouvez obtenir quelque chose de similaire en utilisant la liaison html et un calcul qui dérive le code HTML de la table sous forme de gros bloc de texte, puis l'applique en une fois. Cela corrige le problème de performances, mais l’inconvénient majeur est qu’il limite considérablement les possibilités de liaison dans les lignes de la table. 

Voici un violon illustrant cette approche, ainsi qu'une fonction pouvant être appelée depuis l'intérieur des lignes du tableau pour supprimer un élément de manière vague. Évidemment, ce n'est pas aussi bon que le bon KO, mais si vous avez vraiment besoin de performances époustouflantes, c'est une solution de contournement possible.

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

1
sifriday

J'ai aussi remarqué que le moteur de template Knockout js fonctionnait plus lentement dans IE, je l'ai remplacé par un underscore.js, bien plus rapide.

0
Marcello

Si vous utilisez IE, essayez de fermer les outils de développement.

L'ouverture des outils de développement dans IE ralentit considérablement cette opération. J'ajoute ~ 1000 éléments à un tableau. Lorsque les outils de développement sont ouverts, cela prend environ 10 secondes et IE se fige pendant l'exécution. Lorsque je ferme les outils de développement, l'opération est instantanée et je ne vois aucun ralentissement dans IE.

0
Jon List