web-dev-qa-db-fra.com

Comment améliorer les performances de ng Repeat sur un jeu de données volumineux (angularjs)?

J'ai un énorme ensemble de données de plusieurs milliers de lignes avec environ 10 champs chacun, environ 2 Mo de données. J'ai besoin de l'afficher dans le navigateur. L’approche la plus simple (récupérer des données, les placer dans $scope, laisser ng-repeat="" faire son travail) fonctionne bien, mais le navigateur se bloque pendant environ une demi-minute lorsqu’il commence à insérer des nœuds dans DOM. Comment dois-je aborder ce problème?

Une option consiste à ajouter des lignes à $scope progressivement et à attendre que ngRepeat finisse d'insérer un bloc dans DOM avant de passer au suivant. Mais autant que je sache, ngRepeat ne fait pas rapport quand il a fini de "répéter", alors ça va être moche.

Une autre option consiste à diviser les données sur le serveur en pages et à les récupérer dans plusieurs requêtes, mais c'est encore plus laid.

J'ai parcouru la documentation Angular à la recherche de quelque chose comme ng-repeat="data in dataset" ng-repeat-steps="500", mais je n'ai rien trouvé. Je suis assez nouveau pour Angular façons, il est donc possible que je manque complètement le point. Quelles sont les meilleures pratiques à ce sujet?

163
n1313

Je suis d'accord avec @ AndreM96 sur le fait que la meilleure approche consiste à n'afficher qu'un nombre limité de lignes, une UX plus rapide et de meilleure qualité, ce qui pourrait être fait avec une pagination ou un défilement infini.

Le défilement infini avec Angular est vraiment simple avec le filtre limitTo . Vous devez juste définir la limite initiale et lorsque l'utilisateur demande plus de données (j'utilise un bouton pour plus de simplicité), vous incrémentez la limite.

<table>
    <tr ng-repeat="d in data | limitTo:totalDisplayed"><td>{{d}}</td></tr>
</table>
<button class="btn" ng-click="loadMore()">Load more</button>

//the controller
$scope.totalDisplayed = 20;

$scope.loadMore = function () {
  $scope.totalDisplayed += 20;  
};

$scope.data = data;

Voici un JsBin .

Cette approche pourrait poser problème pour les téléphones, car ils traînent généralement lors du défilement de nombreuses données. Dans ce cas, je pense qu'une pagination convient mieux.

Pour ce faire, vous aurez besoin du filtre limitTo ainsi que d'un filtre personnalisé pour définir le point de départ des données affichées.

Voici un JSBin avec une pagination.

156
Bertrand

L'approche la plus chaude - et sans doute la plus évolutive - pour relever ces défis avec des jeux de données volumineux est incarnée par l'approche de directive Ionic's collectionRepeat et par d'autres implémentations similaires. Un terme sophistiqué pour cela est 'élimination d'occlusion' , mais vous pouvez le résumer comme suit: ne limitez pas simplement le nombre d'éléments DOM rendus à un nombre paginé arbitraire (mais toujours élevé) comme 50, 100, 500 ... à la place, ne limitent que le nombre d'éléments visibles par l'utilisateur .

Si vous faites quelque chose comme ce qu'on appelle communément le "défilement infini", vous réduisez quelque peu le nombre de DOM initial initial , mais il gonfle rapidement après quelques rafraîchissements. , parce que tous ces nouveaux éléments sont simplement ajoutés en bas. Le défilement fait l’objet d’une analyse, car il s’agit avant tout du nombre d’éléments. Il n'y a rien d'infini à ce sujet.

Tandis que l'approche collectionRepeat consiste à n'utiliser que le nombre d'éléments pouvant être inséré dans la fenêtre d'affichage, puis à les les recycler . Lorsqu'un élément disparaît de la vue, il est détaché de l'arborescence de rendu, rempli de données pour un nouvel élément de la liste, puis rattaché à l'arborescence de rendu à l'autre bout de la liste. C’est le moyen le plus rapide que l’homme connaisse pour obtenir de nouvelles informations dans le DOM, en utilisant un nombre limité d’éléments existants, plutôt que le cycle traditionnel de création/destruction… création/destruction. En utilisant cette approche, vous pouvez réellement implémenter un défilement infini .

Notez qu'il n'est pas nécessaire d'utiliser Ionic pour utiliser/hack/adapter collectionRepeat, ni aucun autre outil du même type. C'est pourquoi ils l'appellent open-source. :-) (Ceci dit, l'équipe Ionic fait des choses assez ingénieuses, dignes de votre attention.)


Il y a au moins un excellent exemple de faire quelque chose de très similaire dans React. Au lieu de recycler les éléments dont le contenu est mis à jour, vous choisissez simplement de ne rien restituer dans l'arborescence. C'est rapide sur 5000 articles, bien que leur implémentation très simple de POC permette un peu de scintillement ...


Aussi ... pour faire écho à d'autres publications, utiliser track by est très utile, même avec des ensembles de données plus petits. Considérez cela comme obligatoire.

39
XML

Je recommande de voir ceci:

Optimisation d'AngularJS: 1200 ms à 35 ms

ils ont fait une nouvelle directive en optimisant ng-repeat à 4 parties:

Optimisation n ° 1: Mettre en cache les éléments DOM

Optimisation n ° 2: observateurs agrégés

Optimisation n ° 3: création d'élément différé

Optimisation n ° 4: contourner les observateurs pour les éléments cachés

le projet est ici sur github:

Usage:

1- inclure ces fichiers dans votre application d'une seule page:

  • core.js
  • scalyr.js
  • slyEvaluate.js
  • slyRepeat.js

2- ajouter une dépendance de module:

var app = angular.module("app", ['sly']);

3- remplacer ng-repeat

<tr sly-repeat="m in rows"> .....<tr>

Prendre plaisir!

35
pixparker

À côté de tous les conseils ci-dessus tels que piste par piste et boucles plus petites, celle-ci m'a également beaucoup aidé

<span ng-bind="::stock.name"></span>

ce morceau de code affichera le nom une fois chargé et cessera de le regarder par la suite. De même, pour ng-repeats, il pourrait être utilisé comme

<div ng-repeat="stock in ::ctrl.stocks">{{::stock.name}}</div>

cependant, cela ne fonctionne que pour AngularJS version 1.3 et ultérieure. De http://www.befundoo.com/blog/optimizing-ng-repeat-in-angularjs/

14
Shilan

Si toutes vos lignes ont la même hauteur, vous devriez absolument jeter un coup d'œil à la virtualisation ng-repeat: http://kamilkp.github.io/angular-vs-repeat/

Cette démo est très prometteuse (et supporte le défilement inertiel)

11
bartekp

Vous pouvez utiliser "track by" pour augmenter les performances:

<div ng-repeat="a in arr track by a.trackingKey">

Plus rapide que:

<div ng-repeat="a in arr">

ref: https://www.airpair.com/angularjs/posts/angularjs-performance-large-applications

11
user1920302

Règle n ° 1: Ne jamais laisser l'utilisateur attendre quoi que ce soit.

Cela dit, une page en pleine croissance nécessitant 10 secondes apparaît bien plus rapidement que 3 secondes d’attente avant un écran vide et que tout se présente en même temps.

Ainsi, au lieu de make la page rapide, laissez la page apparaître rapide, même si le résultat final est plus lent:

function applyItemlist(items){
    var item = items.shift();
    if(item){
        $timeout(function(){
            $scope.items.Push(item);
            applyItemlist(items);
        }, 0); // <-- try a little gap of 10ms
    }
}

Le code ci-dessus laisse apparaître la liste s'agrandissant ligne par ligne et est toujours plus lent que le rendu en une fois. mais pour l'utilisateur cela semble être plus rapide.

9
Steffomio

Le défilement virtuel est un autre moyen d’améliorer les performances de défilement lorsqu’il s’agit de listes énormes et de jeux de données volumineux.

Une façon de réaliser cela consiste à utiliser Matériau angulairemd-virtual-repeat comme il est démontré sur ce démonstration de 50 000 éléments

Tiré directement de la documentation de virtual repeat:

Virtual repeat est un substitut limité à ng-repeat qui ne restitue que suffisamment de nœuds dom pour remplir le conteneur et les recycler à mesure que l'utilisateur fait défiler.

9
Akis Tofas

Une autre version @Steffomio

Au lieu d'ajouter chaque élément individuellement, nous pouvons ajouter des éléments par morceaux.

// chunks function from here: 
// http://stackoverflow.com/questions/8495687/split-array-into-chunks#11764168
var chunks = chunk(folders, 100);

//immediate display of our first set of items
$scope.items = chunks[0];

var delay = 100;
angular.forEach(chunks, function(value, index) {
    delay += 100;

    // skip the first chuck
    if( index > 0 ) {
        $timeout(function() {
            Array.prototype.Push.apply($scope.items,value);
        }, delay);
    }       
});
5
Luevano
Created a directive (ng-repeat with lazy loading) 

qui charge les données quand il atteint le bas de la page et supprime la moitié des données précédemment chargées et quand il atteint de nouveau le haut de la div, les données précédentes (en fonction du numéro de page) seront chargées, supprimant la moitié des données actuelles So on DOM à la fois, seules des données limitées sont présentes, ce qui peut améliorer les performances au lieu de restituer des données complètes à la charge.

CODE HTML:

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
    <script data-require="[email protected]" src="https://code.angularjs.org/1.3.20/angular.js" data-semver="1.3.20"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="ListController">
  <div class="row customScroll" id="customTable" datafilter pagenumber="pageNumber" data="rowData" searchdata="searchdata" itemsPerPage="{{itemsPerPage}}"  totaldata="totalData"   selectedrow="onRowSelected(row,row.index)"  style="height:300px;overflow-y: auto;padding-top: 5px">

    <!--<div class="col-md-12 col-xs-12 col-sm-12 assign-list" ng-repeat="row in CRGC.rowData track by $index | orderBy:sortField:sortReverse | filter:searchFish">-->
    <div class="col-md-12 col-xs-12 col-sm-12 pdl0 assign-list" style="padding:10px" ng-repeat="row in rowData" ng-hide="row[CRGC.columns[0].id]=='' && row[CRGC.columns[1].id]==''">
        <!--col1-->

        <div ng-click ="onRowSelected(row,row.index)"> <span>{{row["sno"]}}</span> <span>{{row["id"]}}</span> <span>{{row["name"]}}</span></div>
      <!--   <div class="border_opacity"></div> -->
    </div>

</div>

  </body>

</html>

CODE angulaire:

var app = angular.module('plunker', []);
var x;
ListController.$inject = ['$scope', '$timeout', '$q', '$templateCache'];

function ListController($scope, $timeout, $q, $templateCache) {
  $scope.itemsPerPage = 40;
  $scope.lastPage = 0;
  $scope.maxPage = 100;
  $scope.data = [];
  $scope.pageNumber = 0;


  $scope.makeid = function() {
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    for (var i = 0; i < 5; i++)
      text += possible.charAt(Math.floor(Math.random() * possible.length));

    return text;
  }


  $scope.DataFormFunction = function() {
      var arrayObj = [];
      for (var i = 0; i < $scope.itemsPerPage*$scope.maxPage; i++) {
          arrayObj.Push({
              sno: i + 1,
              id: Math.random() * 100,
              name: $scope.makeid()
          });
      }
      $scope.totalData = arrayObj;
      $scope.totalData = $scope.totalData.filter(function(a,i){ a.index = i; return true; })
      $scope.rowData = $scope.totalData.slice(0, $scope.itemsperpage);
    }
  $scope.DataFormFunction();

  $scope.onRowSelected = function(row,index){
    console.log(row,index);
  }

}

angular.module('plunker').controller('ListController', ListController).directive('datafilter', function($compile) {
  return {
    restrict: 'EAC',
    scope: {
      data: '=',
      totalData: '=totaldata',
      pageNumber: '=pagenumber',
      searchdata: '=',
      defaultinput: '=',
      selectedrow: '&',
      filterflag: '=',
      totalFilterData: '='
    },
    link: function(scope, elem, attr) {
      //scope.pageNumber = 0;
      var tempData = angular.copy(scope.totalData);
      scope.totalPageLength = Math.ceil(scope.totalData.length / +attr.itemsperpage);
      console.log(scope.totalData);
      scope.data = scope.totalData.slice(0, attr.itemsperpage);
      elem.on('scroll', function(event) {
        event.preventDefault();
      //  var scrollHeight = angular.element('#customTable').scrollTop();
      var scrollHeight = document.getElementById("customTable").scrollTop
        /*if(scope.filterflag && scope.pageNumber != 0){
        scope.data = scope.totalFilterData;
        scope.pageNumber = 0;
        angular.element('#customTable').scrollTop(0);
        }*/
        if (scrollHeight < 100) {
          if (!scope.filterflag) {
            scope.scrollUp();
          }
        }
        if (angular.element(this).scrollTop() + angular.element(this).innerHeight() >= angular.element(this)[0].scrollHeight) {
          console.log("scroll bottom reached");
          if (!scope.filterflag) {
            scope.scrollDown();
          }
        }
        scope.$apply(scope.data);

      });

      /*
       * Scroll down data append function
       */
      scope.scrollDown = function() {
          if (scope.defaultinput == undefined || scope.defaultinput == "") { //filter data append condition on scroll
            scope.totalDataCompare = scope.totalData;
          } else {
            scope.totalDataCompare = scope.totalFilterData;
          }
          scope.totalPageLength = Math.ceil(scope.totalDataCompare.length / +attr.itemsperpage);
          if (scope.pageNumber < scope.totalPageLength - 1) {
            scope.pageNumber++;
            scope.lastaddedData = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage, (+attr.itemsperpage) + (+scope.pageNumber * attr.itemsperpage));
            scope.data = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage - 0.5 * (+attr.itemsperpage), scope.pageNumber * attr.itemsperpage);
            scope.data = scope.data.concat(scope.lastaddedData);
            scope.$apply(scope.data);
            if (scope.pageNumber < scope.totalPageLength) {
              var divHeight = $('.assign-list').outerHeight();
              if (!scope.moveToPositionFlag) {
                angular.element('#customTable').scrollTop(divHeight * 0.5 * (+attr.itemsperpage));
              } else {
                scope.moveToPositionFlag = false;
              }
            }


          }
        }
        /*
         * Scroll up data append function
         */
      scope.scrollUp = function() {
          if (scope.defaultinput == undefined || scope.defaultinput == "") { //filter data append condition on scroll
            scope.totalDataCompare = scope.totalData;
          } else {
            scope.totalDataCompare = scope.totalFilterData;
          }
          scope.totalPageLength = Math.ceil(scope.totalDataCompare.length / +attr.itemsperpage);
          if (scope.pageNumber > 0) {
            this.positionData = scope.data[0];
            scope.data = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage - 0.5 * (+attr.itemsperpage), scope.pageNumber * attr.itemsperpage);
            var position = +attr.itemsperpage * scope.pageNumber - 1.5 * (+attr.itemsperpage);
            if (position < 0) {
              position = 0;
            }
            scope.TopAddData = scope.totalDataCompare.slice(position, (+attr.itemsperpage) + position);
            scope.pageNumber--;
            var divHeight = $('.assign-list').outerHeight();
            if (position != 0) {
              scope.data = scope.TopAddData.concat(scope.data);
              scope.$apply(scope.data);
              angular.element('#customTable').scrollTop(divHeight * 1 * (+attr.itemsperpage));
            } else {
              scope.data = scope.TopAddData;
              scope.$apply(scope.data);
              angular.element('#customTable').scrollTop(divHeight * 0.5 * (+attr.itemsperpage));
            }
          }
        }
    }
  };
});

démo avec directive

Another Solution: If you using UI-grid in the project then  same implementation is there in UI grid with infinite-scroll.

En fonction de la hauteur de la division, il charge les données et lors du défilement, de nouvelles données sont ajoutées et les données précédentes supprimées.

Code HTML:

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <link rel="stylesheet" href="https://cdn.rawgit.com/angular-ui/bower-ui-grid/master/ui-grid.min.css" type="text/css" />
    <script data-require="[email protected]" src="https://code.angularjs.org/1.3.20/angular.js" data-semver="1.3.20"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-grid/4.0.6/ui-grid.js"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="ListController">
     <div class="input-group" style="margin-bottom: 15px">
      <div class="input-group-btn">
        <button class='btn btn-primary' ng-click="resetList()">RESET</button>
      </div>
      <input class="form-control" ng-model="search" ng-change="abc()">
    </div>

    <div data-ui-grid="gridOptions" class="grid" ui-grid-selection  data-ui-grid-infinite-scroll style="height :400px"></div>

    <button ng-click="getProductList()">Submit</button>
  </body>

</html>

Code angulaire:

var app = angular.module('plunker', ['ui.grid', 'ui.grid.infiniteScroll', 'ui.grid.selection']);
var x;
angular.module('plunker').controller('ListController', ListController);
ListController.$inject = ['$scope', '$timeout', '$q', '$templateCache'];

function ListController($scope, $timeout, $q, $templateCache) {
    $scope.itemsPerPage = 200;
    $scope.lastPage = 0;
    $scope.maxPage = 5;
    $scope.data = [];

    var request = {
        "startAt": "1",
        "noOfRecords": $scope.itemsPerPage
    };
    $templateCache.put('ui-grid/selectionRowHeaderButtons',
        "<div class=\"ui-grid-selection-row-header-buttons \" ng-class=\"{'ui-grid-row-selected': row.isSelected}\" ><input style=\"margin: 0; vertical-align: middle\" type=\"checkbox\" ng-model=\"row.isSelected\" ng-click=\"row.isSelected=!row.isSelected;selectButtonClick(row, $event)\">&nbsp;</div>"
    );


    $templateCache.put('ui-grid/selectionSelectAllButtons',
        "<div class=\"ui-grid-selection-row-header-buttons \" ng-class=\"{'ui-grid-all-selected': grid.selection.selectAll}\" ng-if=\"grid.options.enableSelectAll\"><input style=\"margin: 0; vertical-align: middle\" type=\"checkbox\" ng-model=\"grid.selection.selectAll\" ng-click=\"grid.selection.selectAll=!grid.selection.selectAll;headerButtonClick($event)\"></div>"
    );

    $scope.gridOptions = {
        infiniteScrollDown: true,
        enableSorting: false,
        enableRowSelection: true,
        enableSelectAll: true,
        //enableFullRowSelection: true,
        columnDefs: [{
            field: 'sno',
            name: 'sno'
        }, {
            field: 'id',
            name: 'ID'
        }, {
            field: 'name',
            name: 'My Name'
        }],
        data: 'data',
        onRegisterApi: function(gridApi) {
            gridApi.infiniteScroll.on.needLoadMoreData($scope, $scope.loadMoreData);
            $scope.gridApi = gridApi;
        }
    };
    $scope.gridOptions.multiSelect = true;
    $scope.makeid = function() {
        var text = "";
        var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        for (var i = 0; i < 5; i++)
            text += possible.charAt(Math.floor(Math.random() * possible.length));

        return text;
    }
    $scope.abc = function() {
        var a = $scope.search;
        x = $scope.searchData;
        $scope.data = x.filter(function(arr, y) {
            return arr.name.indexOf(a) > -1
        })
        console.log($scope.data);
        if ($scope.gridApi.grid.selection.selectAll)
            $timeout(function() {
                $scope.gridApi.selection.selectAllRows();
            }, 100);
    }


    $scope.loadMoreData = function() {
        var promise = $q.defer();
        if ($scope.lastPage < $scope.maxPage) {
            $timeout(function() {
                var arrayObj = [];
                for (var i = 0; i < $scope.itemsPerPage; i++) {
                    arrayObj.Push({
                        sno: i + 1,
                        id: Math.random() * 100,
                        name: $scope.makeid()
                    });
                }

                if (!$scope.search) {
                    $scope.lastPage++;
                    $scope.data = $scope.data.concat(arrayObj);
                    $scope.gridApi.infiniteScroll.dataLoaded();
                    console.log($scope.data);
                    $scope.searchData = $scope.data;
                    // $scope.data = $scope.searchData;
                    promise.resolve();
                    if ($scope.gridApi.grid.selection.selectAll)
                        $timeout(function() {
                            $scope.gridApi.selection.selectAllRows();
                        }, 100);
                }


            }, Math.random() * 1000);
        } else {
            $scope.gridApi.infiniteScroll.dataLoaded();
            promise.resolve();
        }
        return promise.promise;
    };

    $scope.loadMoreData();

    $scope.getProductList = function() {

        if ($scope.gridApi.selection.getSelectedRows().length > 0) {
            $scope.gridOptions.data = $scope.resultSimulatedData;
            $scope.mySelectedRows = $scope.gridApi.selection.getSelectedRows(); //<--Property undefined error here
            console.log($scope.mySelectedRows);
            //alert('Selected Row: ' + $scope.mySelectedRows[0].id + ', ' + $scope.mySelectedRows[0].name + '.');
        } else {
            alert('Select a row first');
        }
    }
    $scope.getSelectedRows = function() {
        $scope.mySelectedRows = $scope.gridApi.selection.getSelectedRows();
    }
    $scope.headerButtonClick = function() {

        $scope.selectAll = $scope.grid.selection.selectAll;

    }
}

Démo avec grille d'interface utilisateur avec démo à défilement infini

0
ankesh jain

Parfois, que se passe-t-il, vous obtenez les données du serveur (ou du back-end) en quelques ms (par exemple, je suis en supposant que cela dure 100 ms) mais il faut plus de temps pour s'afficher dans notre page Web (disons qu'il faut 900 ms pour s'afficher).

Donc, ce qui se passe ici est de 800ms. Il suffit de rendre une page Web.

Ce que j’ai fait dans mon application Web, c’est pagination (ou vous pouvez utiliser un défilement infini également) pour afficher liste de données. Disons que je montre 50 données/page.

Donc, je ne vais pas charger toutes les données en même temps, seulement 50 données que je charge au départ, ce qui ne prend que 50 ms (je suppose ici).

le temps total a donc été réduit ici de 900 ms à 150 ms, une fois que l'utilisateur a demandé à la page suivante d'afficher les 50 données suivantes, etc.

J'espère que cela vous aidera à améliorer les performances. Tous les meilleurs

0
UniCoder