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

86

Je ne fais que commencer avec Knockout.js (j'ai toujours voulu l'essayer, mais maintenant j'ai enfin une excuse!) - Cependant, je rencontre de très mauvais problèmes de performances lors de la liaison d'une table à un ensemble relativement petit de données (environ 400 lignes environ).

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 forboucle ci-dessus prend environ 30 secondes environ 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 forboucle se termine en un clin d'œil. En d'autres termes, la pushméthode de l' observableArrayobjet 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 à pushfaire un recalcul lourd à chaque fois que je l'appelle, comme peut-être la reconstruction d'objets DOM liés. Existe-t-il un moyen de retarder ce recalcul, ou peut-être d'insérer tous mes articles en même temps?

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

MISE À 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 sais pas à quelle vitesse ce serait sans Knockout (juste en ajoutant des lignes via le DOM), mais j'ai le sentiment que ce serait beaucoup plus rapide que 10 secondes.

MISE À JOUR 2:

Par autre conseil ci-dessous, j'ai donné un coup de feu à jQuery.tmpl (qui est nativement pris en charge par KnockOut), et ce moteur de création de modèles dessinera environ 400 lignes en un peu plus de 3 secondes. Cela semble être la meilleure approche, à l'exception d'une solution qui chargerait dynamiquement plus de données lorsque vous faites défiler.

Mike Christensen
la source
1
Utilisez-vous une liaison knockout foreach ou une liaison modèle avec foreach. Je me demande simplement si l'utilisation d'un modèle et l'inclusion de jquery tmpl au lieu du moteur de modèle natif peuvent faire une différence.
madcapnmckay
1
@MikeChristensen - Knockout a son propre moteur de template natif associé aux liaisons (foreach, with). Il prend également en charge d'autres moteurs de modèles, à savoir jquery.tmpl. Lisez ici pour plus de détails. Je n'ai pas fait de benchmarking avec différents moteurs, donc 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.
madcapnmckay
2
Étant donné que nous venons de recevoir IE7 il y a quelques mois, je pense que IE9 sera déployé vers l'été 2019. Oh, nous sommes tous aussi sur WinXP .. Blech.
Mike Christensen
1
ps, La raison pour laquelle cela semble lent est que vous ajoutez individuellement 400 éléments à ce tableau observable . Pour chaque modification apportée à l'observable, la vue doit être rendue pour tout ce qui dépend de ce tableau. Pour les modèles complexes et de nombreux éléments à ajouter, cela représente beaucoup de surcharge lorsque vous auriez pu simplement mettre à jour le tableau en même temps en le définissant sur une instance différente. Au moins alors, le nouveau rendu serait effectué une fois.
Jeff Mercado du
1
J'ai trouvé un moyen plus rapide et plus soigné (rien hors de la boîte). en utilisant le valueHasMutatedfait. vérifiez la réponse si vous avez le temps.
super cool

Réponses:

16

Comme suggéré dans les commentaires.

Knockout a son propre moteur de modèle natif associé aux liaisons (foreach, with). Il prend également en charge d'autres moteurs de modèles, à savoir jquery.tmpl. Lisez ici pour plus de détails. Je n'ai pas fait de benchmarking avec différents moteurs, donc 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 prend en charge tout moteur de création de modèles js, si quelqu'un a écrit l'adaptateur pour celui-ci. Vous voudrez peut-être en essayer d'autres, car jquery tmpl doit être remplacé par JsRender .

madcapnmckay
la source
J'obtiens une bien meilleure performance avec jquery.tmpldonc je vais l'utiliser. Je pourrais étudier d'autres moteurs et écrire le mien si j'ai un peu de temps. Merci!
Mike Christensen
1
@MikeChristensen - utilisez-vous toujours des data-bindinstructions dans votre modèle jQuery, ou utilisez-vous la syntaxe $ {code}?
ericb
@ericb - Avec le nouveau code, j'utilise la ${code}syntaxe et c'est beaucoup plus rapide. J'ai également essayé de faire fonctionner Underscore.js, mais je n'ai pas encore eu de chance (la <% .. %>syntaxe interfère avec ASP.NET), et il ne semble pas encore y avoir de support JsRender.
Mike Christensen
1
@MikeChristensen - ok, alors c'est logique. Le moteur de modèle natif de KO n'est pas nécessairement aussi inefficace. Lorsque vous utilisez la syntaxe $ {code}, vous n'obtenez aucune liaison de données sur ces éléments (ce qui améliore les performances). Ainsi, si vous modifiez une propriété de a ResultRow, cela ne mettra pas à jour l'interface utilisateur (vous devrez mettre à jour le projectsobservableArray qui forcera un re-rendu de votre table). $ {} peut certainement être avantageux si vos données sont à peu près en lecture seule
ericb
4
Nécromancie! jquery.tmpl n'est plus en développement
Alex Larzelere
50

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

Un meilleur modèle est d'obtenir une référence à notre tableau sous-jacent, de pousser dessus, puis d'appeler .valueHasMutated (). Désormais, nos abonnés ne recevront qu'une seule notification indiquant que le tableau a changé.

Jim G.
la source
13

Utilisez la pagination avec KO en plus d'utiliser $ .map.

J'ai eu le même problème avec un grand ensemble de données de 1400 enregistrements jusqu'à ce que j'utilise la pagination avec knockout. Utiliser $.mappour charger les enregistrements a fait une énorme différence, mais le temps de rendu du DOM était toujours horrible. Ensuite, j'ai essayé d'utiliser la pagination et cela a rendu l'éclairage de mon ensemble de données plus rapide et plus convivial. Une taille de page de 50 a rendu l'ensemble de données beaucoup moins écrasant et a considérablement réduit le nombre d'éléments DOM.

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

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

Tim Santeford
la source
11

KnockoutJS propose d'excellents tutoriels, en particulier celui sur le chargement et l'enregistrement de données

Dans leur cas, ils extraient des données en utilisant getJSON()ce 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);
    });    
}
deltree
la source
1
Certainement une grande amélioration, mais l' self.tasks(mappedTasks)exécution prend environ 10 secondes (avec 400 lignes). Je pense que ce n'est toujours pas acceptable.
Mike Christensen
Je conviens que 10 secondes ne sont pas acceptables. En utilisant knockoutjs, je ne suis pas sûr de ce qui est mieux qu'une carte, je vais donc ajouter cette question à mes favoris et chercher une meilleure réponse.
deltree
1
D'accord. La réponse mérite certainement une +1pour simplifier mon code et augmenter considérablement la vitesse. Peut-être que quelqu'un a une explication plus détaillée de ce qu'est le goulot d'étranglement.
Mike Christensen
9

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

Si vous essayez de lier 400 lignes à une table à l'aide d'une foreachliaison, vous allez avoir du mal à pousser autant de KO dans le DOM.

KO fait des choses très intéressantes en utilisant la foreachliaison, 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 emprunté la longue route sombre d'essayer de lier de grands ensembles de données à des tables / grilles, et vous finissez par devoir séparer / paginer les données localement.

KoGrid fait tout cela. Il a été conçu pour rendre uniquement les lignes que le spectateur peut voir sur la page, puis virtualiser les autres lignes jusqu'à ce qu'elles soient nécessaires. Je pense que vous trouverez ses performances sur 400 articles bien meilleures que celles que vous rencontrez.

Ericb
la source
1
Cela semble être complètement cassé sur IE7 (aucun des échantillons ne fonctionne), sinon ce serait génial!
Mike Christensen
Heureux de l'examiner - KoGrid est toujours en développement actif. Cependant, cela répond-il au moins à votre question concernant les performances?
ericb
1
Ouaip! Cela confirme mon soupçon initial que le moteur de modèle KO par défaut est assez lent. Si vous avez besoin de quelqu'un pour cochon d'Inde KoGrid pour vous, je serais heureux de le faire. Sonne exactement ce dont nous avons besoin!
Mike Christensen
Zut. Cela a l'air vraiment bien! Malheureusement, plus de 50% des utilisateurs de mon application utilisent IE7!
Jim G.
Intéressant, de nos jours, nous devons soutenir à contrecœur IE11. Les choses se sont améliorées ces 7 dernières années.
MrBoJangles
5

Une solution pour éviter de verrouiller le navigateur lors du rendu d'un très grand tableau consiste à «étrangler» le tableau de sorte que seuls quelques éléments soient ajoutés à la fois, avec un sommeil 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 peut entraîner une amélioration massive de l'expérience utilisateur, car l'utilisateur peut ne voir que le premier lot de lignes avant de devoir faire défiler.

teh_senaus
la source
J'aime cette solution, mais plutôt que setTimeout à chaque itération, je recommande d'exécuter setTimout uniquement toutes les 20 itérations ou plus, car chaque fois prend également trop de temps à charger. Je vois que tu fais ça avec le +20, mais ce n'était pas évident pour moi à première vue.
charlierlee
5

Profiter de push () acceptant des arguments variables a donné les meilleures performances dans mon cas. 1300 lignes se chargeaient pendant 5973 ms (~ 6 sec.). Avec cette optimisation, le temps de chargement a été réduit à 914 ms (<1 s)
, soit une amélioration de 84,7%!

Plus d'informations sur Pousser des éléments vers 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);
};
Mitaka
la source
4

J'ai eu affaire à d'énormes volumes de données qui m'arrivaient valueHasMutatedfonctionnait comme un charme.

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 l'appel (4), les données du tableau seront chargées dans le observableArray requis qui est this.projectsautomatiquement.

si vous avez le temps, jetez un œil à ceci et juste au cas où vous auriez un problème, faites-le moi savoir

Astuce ici: En faisant comme ça, si en cas de dépendances (calculées, abonnés, etc.), on peut éviter au niveau push et nous pouvons les faire exécuter en une seule fois après l'appel (4).

super cool
la source
1
Le problème n'est pas trop d'appels à push, le problème est que même un seul appel à pousser entraînera de longs temps de rendu. Si un tableau a 1000 éléments liés à a foreach, pousser un seul élément redonne tout le foreach et vous payez un coût de temps de rendu élevé.
Légère
1

Une solution de contournement possible, en combinaison avec l'utilisation de jQuery.tmpl, consiste à pousser des éléments à la fois vers le tableau observable de manière asynchrone, en utilisant 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
  }
}

De cette façon, lorsque vous n'ajoutez qu'un seul élément à la fois, le navigateur / knockout.js peut prendre son temps pour manipuler le DOM en conséquence, sans que le navigateur ne soit complètement bloqué pendant plusieurs secondes, afin que l'utilisateur puisse faire défiler la liste simultanément.

gnab
la source
2
Cela forcera N nombre de mises à jour du DOM, ce qui entraînera un temps de rendu total beaucoup plus long que de tout faire en même temps.
Fredrik C
C'est bien sûr correct. Le fait est, cependant, que la combinaison de N étant un grand nombre et le fait de pousser un élément dans le tableau des projets déclenchant une quantité importante d'autres mises à jour ou calculs DOM, peut provoquer le blocage du navigateur et vous proposer de supprimer l'onglet. En ayant un délai d'expiration, soit par élément, soit par 10, 100 ou un autre nombre d'éléments, le navigateur sera toujours réactif.
gnab
2
Je dirais que c'est la mauvaise approche dans le cas général où la mise à jour totale ne gèle pas le navigateur, mais c'est quelque chose à utiliser lorsque tous les autres échouent. Pour moi, cela ressemble à une application mal écrite où les problèmes de performances devraient être résolus au lieu de simplement l'empêcher de se figer.
Fredrik C
1
Bien sûr, c'est la mauvaise approche dans le cas général, personne ne serait en désaccord avec vous sur ce point. Il s'agit d'un hack et d'une preuve de concept pour empêcher le gel du navigateur si vous devez effectuer de nombreuses opérations DOM. J'en avais besoin il y a quelques années lors de la liste de plusieurs grands tableaux HTML avec plusieurs liaisons par cellule, ce qui a entraîné l'évaluation de milliers de liaisons, chacune affectant l'état du DOM. La fonctionnalité était temporairement nécessaire pour vérifier l'exactitude de la réimplémentation d'une application de bureau Excel en tant qu'application Web. Ensuite, cette solution a parfaitement fonctionné.
gnab
Le commentaire était principalement pour les autres à lire pour ne pas supposer que c'était la méthode préférée. J'ai supposé que vous saviez ce que vous faisiez.
Fredrik C
1

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

Mes expériences se concentrent sur le temps de manipulation du DOM. Donc, avant d'entrer dans cela, il vaut vraiment la peine de suivre les points ci-dessus à propos de l'insertion dans un tableau JS avant de créer un tableau observable, etc.

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


1: Un motif pour enrouler un spinner de chargement autour du rendu lent, puis le masquer en utilisant afterRender

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

Ce n'est pas vraiment une solution au problème de performances, mais montre qu'un délai est probablement inévitable si vous bouclez sur des milliers d'éléments et qu'il utilise un modèle dans lequel vous pouvez vous assurer qu'un spinner de chargement apparaît avant la longue opération KO, puis masquez après. Donc, cela améliore au moins l'UX.

Assurez-vous de pouvoir charger un 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)

Cachez 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 vieille technique de l'époque où je travaillais sur un décodeur avec Opera, la construction de l'interface utilisateur en utilisant la manipulation DOM. C'était terriblement lent, donc la solution était de stocker de gros morceaux de HTML sous forme de chaînes et de charger les chaînes en définissant la propriété innerHTML.

Quelque chose de similaire peut être réalisé en utilisant la liaison html et un calcul qui dérive le HTML de la table sous forme d'un gros morceau de texte, puis l'applique en une seule fois. Cela résout le problème de performances, mais l'inconvénient majeur est que cela limite considérablement ce que vous pouvez faire avec la liaison à l'intérieur de chaque ligne de table.

Voici un violon qui montre cette approche, ainsi qu'une fonction qui peut être appelée à l'intérieur des lignes de la table pour supprimer un élément d'une manière vaguement KO. De toute évidence, ce n'est pas aussi bon qu'un KO correct, mais si vous avez vraiment besoin de performances fulgurantes (ish), c'est une solution de contournement possible.

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

sifriday
la source
1

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 que cela se produit. Lorsque je ferme les outils de développement, l'opération est instantanée et je ne vois aucun ralentissement dans IE.

Jon List
la source
0

J'ai également remarqué que le moteur de modèle Knockout js fonctionne plus lentement dans IE, je l'ai remplacé par underscore.js, fonctionne beaucoup plus rapidement.

Marcello
la source
Comment avez-vous fait cela s'il vous plaît?
Stu Harper
@StuHarper J'ai importé la bibliothèque de soulignements, puis dans main.js j'ai suivi les étapes décrites dans la section d'intégration des soulignements de knockoutjs.com/documentation/template-binding.html
Marcello
Avec quelle version d'IE cette amélioration s'est-elle produite?
bkwdesign
@bkwdesign J'utilisais IE 10, 11.
Marcello