Comment améliorer la performance de ngRepeat au-dessus d'un énorme ensemble de données (angulaire.js)?

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 mettre dans $scope , laisser ng-repeat="" faire son travail) fonctionne bien, mais il gèle le navigateur pendant environ une demi-minute quand il commence à insérer des noeuds dans DOM. Comment dois-je aborder ce problème?

Une option est d'ajouter des lignes à $scope progressivement et attendre ngRepeat pour finir d'insérer un morceau dans DOM avant de passer au suivant. Mais AFAIK ngRepeat ne fait pas de rapport quand il finit "répéter", donc il va être laid.

l'autre option est de diviser les données sur le serveur en pages et de les récupérer dans des requêtes multiples, mais c'est encore plus laid.

j'ai regardé dans la documentation angulaire à 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 à l'Angulaire manières, de sorte qu'il est il est possible que je passe complètement à côté de l'essentiel. Quelles sont les meilleures pratiques?

153
demandé sur n1313 2013-06-27 20:07:12

12 réponses

je suis d'accord avec @AndreM96 que la meilleure approche est d'afficher seulement un nombre limité de lignes, plus rapide et mieux UX, cela pourrait être fait avec une pagination ou avec un rouleau infini.

scroll infini avec angle est vraiment simple avec limitTo filtre. Vous avez juste à définir la limite initiale et lorsque l'utilisateur demande plus de données (j'utilise un bouton pour la 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;

ici est un JsBin .

cette approche pourrait être un problème pour les téléphones parce que généralement ils sont en retard lors du balayage de beaucoup de données, donc dans ce cas, je pense qu'une pagination convient mieux.

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

voici un JSBin avec une pagination.

150
répondu Bema 2016-07-06 19:36:16

l'approche la plus chaude - et sans doute la plus évolutive - pour surmonter ces défis avec de grands ensembles de données est incarnée par l'approche de ionic collectionRepeat directive et d'autres implémentations comme elle. Un terme fantaisiste pour cela est 'occlusion culling' , mais vous pouvez le résumer comme: ne pas limiter le nombre d'éléments DOM rendus à un nombre paginé arbitraire (mais encore élevé) comme 50, 100, 500... au lieu de cela, la limite seulement autant d'éléments que l'utilisateur peut voir .

si vous faites quelque chose comme ce qui est communément appelé" infinite scrolling", vous réduisez le initial DOM comptent un peu, mais il gonfle rapidement après quelques rafraîchissements, parce que tous ces nouveaux éléments sont juste accrochés sur le fond. Le Scrolling vient à un crawl, parce que le scrolling est tout au sujet du nombre d'éléments. Il n'y a rien infinie à ce sujet.

considérant que, L'approche collectionRepeat consiste à n'utiliser que le nombre d'éléments qui conviennent à viewport, puis à les recycler . Comme un élément tourne hors de vue, il est détaché de l'arbre de rendu, rempli de données pour un nouvel élément dans la liste, puis rattachée à l'arbre de rendu à l'autre extrémité de la liste. C'est le moyen le plus rapide connu à l'homme pour obtenir de nouvelles informations dans et hors de la DOM, rendant l'utilisation d'un ensemble limité d'éléments existants, plutôt que le traditionnel cycle de créer/détruire... créer/détruire. En utilisant cette approche, vous pouvez vraiment mettre en œuvre un infinite scroll.

notez que vous ne devez pas utiliser Ionic pour utiliser/hack/adapt collectionRepeat , ou tout autre outil comme elle. C'est pour ça qu'ils l'appellent open-source. :-) (Cela dit, L'équipe ionique 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. Seulement au lieu de recycler les éléments avec du contenu mis à jour, vous choisissez simplement de ne rien rendre dans l'arbre qui n'est pas en vue. Il brille rapidement sur 5000 articles, bien que leur mise en œuvre POC très simple permet un peu de scintillement...


Aussi... pour faire écho à certains des autres messages, l'utilisation de track by est sérieusement utile, même avec des ensembles de données plus petits. Considérer obligatoire.

36
répondu XML 2015-07-13 13:58:50

je recommande de voir ceci:

Optimisation AngularJS: 1200ms à 35ms

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

optimisation#1: Cache DOM elements

Optimisation N ° 2: Agrégat watchers

Optimisation#3: Reporter élément de la création

Optimisation#4: Dérivation d'observateurs pour les éléments cachés

le projet est ici sur github:

Utilisation:

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

  • core.js
  • scalyr.js
  • slievaluate.js
  • slyRepeat.js

2 - ajouter un module de dépendance:

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

3-Remplacer ng-repeat

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

ENjoY!

34
répondu pixparker 2016-08-26 11:00:17

à côté de tous les conseils ci-dessus comme piste par et des boucles plus petites, celui-ci m'a également aidé beaucoup

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

ce morceau de code imprimerait le nom une fois qu'il a été chargé, et arrêterait de le regarder après cela. De même, pour les répétitions ng, il pourrait être utilisé comme

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

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

12
répondu Shilan 2015-11-27 09:45:11

si toutes vos lignes ont la même hauteur, vous devriez certainement jeter un oeil à la virtualisation ng-repeat: http://kamilkp.github.io/angular-vs-repeat/

Ce démo a l'air très prometteur (et il prend en charge le défilement à inertie)

10
répondu bartekp 2014-04-20 00:44:45

vous pouvez utiliser" track by "pour augmenter la performance:

<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

10
répondu user1920302 2015-10-29 13:59:22

virtual scrolling est une autre façon d'améliorer les performances de défilement lorsqu'il s'agit d'énormes listes et de grands ensembles de données.

une façon de mettre en œuvre ceci est d'utiliser Matériau angulaire md-virtual-repeat comme il est démontré sur cette démo avec 50.000 articles

tiré directement de la documentation de Virtual repeat:

répétition virtuelle est limité pour remplacer ng-repeat qui rend suffisamment de nœuds dom remplir le récipient et de recyclage que l'utilisateur fait défiler.

8
répondu Sarantis Tofas 2018-01-09 12:50:03

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

que dans l'esprit une page de croissance de la vie qui a besoin de 10secondes apparaît beaucoup plus vite que d'attendre 3 secondes avant un écran blanc et obtenir tout à la fois.

donc au lieu de faire la page rapide, il suffit de laisser la page apparaître pour ê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 à il se développe ligne par ligne, et est toujours plus lent que rendre tout à la fois. Mais pour l'utilisateur il semble être plus rapide.

7
répondu Steffomio 2016-04-20 16:03:03

une autre version @Steffomio

au lieu d'ajouter chaque article individuellement, nous pouvons ajouter des articles 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
répondu Luevano 2016-07-11 23:28:05

parfois ce qui s'est passé, vous obtenez les données à partir du serveur (ou back-end) dans quelques ms (par exemple je suis je suppose qu'il 100ms) mais il faut plus de temps pour afficher dans notre page web (disons qu'il faut 900ms pour afficher).

donc, ce qui se passe ici est 800ms Qu'il faut juste pour rendre la page web.

ce que j'ai fait dans mon application web est, j'ai utilisé pagination (ou vous pouvez utiliser infini défilement aussi) pour afficher une liste de données. Disons que je montre 50 données / page.

donc je ne vais pas charger rendre toutes les données à la fois,seulement 50 données que je charge initialement qui ne prend que 50ms (je suppose ici).

donc le temps total ici a diminué de 900ms à 150ms, une fois la demande de l'utilisateur la page suivante puis afficher les 50 prochaines données et ainsi de suite.

espérons que cela vous aidera à améliorer les performances. Tous les meilleurs

0
répondu UniCoder 2017-02-08 11:53:16
Created a directive (ng-repeat with lazy loading) 

qui charge des données quand il atteint au bas de la page et supprimer la moitié des données précédemment chargées et quand il atteint au haut de la div à nouveau les données précédentes(en fonction du nombre de page) sera chargé en enlevant la moitié des données actuelles de sorte que sur DOM à la fois des données limitées est présent ce qui peut conduire à une meilleure performance au lieu de rendre des données entières sur 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="angular.js@1.3.x" 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 sur défiler de nouvelles données seront annexées et les données précédentes seront 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="angular.js@1.3.x" 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 de l'INTERFACE utilisateur de la grille avec un nombre infini de défilement de la Démo

0
répondu ankesh jain 2017-09-11 06:29:10

pour les ensembles de données volumineux et les valeurs multiples, il est préférable d'utiliser ng-options plutôt que ng-repeat .

ng-repeat est lent parce qu'il boucle toutes les valeurs à venir, mais ng-options affiche simplement l'option select.

ng-options='state.StateCode as state.StateName for state in States'>

beaucoup plus rapide que

<option ng-repeat="state in States" value="{{state.StateCode}}">
    {{state.StateName }}
</option>
-2
répondu Ghebrehiywet 2016-05-24 16:58:00