Как повысить производительность ngRepeat над огромным набором данных (angular.js)?

У меня есть огромный набор данных из нескольких тысяч строк с примерно 10 полями каждый, около 2 МБ данных. Мне нужно отобразить его в браузере. Самый простой подход (выборка данных, вставка в $scope, пусть ng-repeat="" выполняет свою работу) отлично работает, но замораживает браузер примерно на половину минуты, когда он начинает вставлять узлы в DOM. Как мне подойти к этой проблеме?

Один из вариантов заключается в том, чтобы добавить строки в $scope поэтапно и подождать, пока ngRepeat закончит вставлять один кусок в DOM, прежде чем переходить к следующему. Но AFAIK ngRepeat не отчитывается, когда заканчивается "повторение", поэтому он будет уродлив.

Другим вариантом является разделение данных на сервере на страницы и выборка их в нескольких запросах, но это еще более уродливое.

Я просмотрел документацию Angular в поисках чего-то вроде ng-repeat="data in dataset" ng-repeat-steps="500", но ничего не нашел. Я новичок в способах Angular, поэтому вполне возможно, что я полностью потерял точку. Каковы наилучшие методы?

Ответ 1

Я согласен с @AndreM96, что лучший подход - отображать только ограниченное количество строк, быстрее и лучше UX, это можно сделать с разбиением на страницы или с бесконечным прокруткой.

Бесконечный свиток с Angular действительно прост с фильтром limitTo. Вам просто нужно установить начальный предел, и когда пользователь запрашивает больше данных (я использую кнопку для простоты), вы увеличиваете лимит.

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

Вот JsBin.

Этот подход может быть проблемой для телефонов, потому что обычно они отстают при прокрутке большого количества данных, поэтому в этом случае я думаю, что разбиение на страницы лучше подходит.

Для этого вам понадобится фильтр limit, а также настраиваемый фильтр, чтобы определить начальную точку отображаемых данных.

Вот JSBin с разбивкой на страницы.

Ответ 2

Самый жаркий - и, возможно, самый масштабируемый - подход к преодолению этих проблем с большими наборами данных воплощен в подходе Ionic collectionRepeat директивы и других подобных им реализаций. Примером этого является 'occlusion culling', но вы можете суммировать его как: не просто ограничивать количество отображаемых элементов DOM произвольным (но все же высокий) число в разбивке по страницам, например 50, 100, 500..., ограничивает только столько элементов, сколько может видеть пользователь.

Если вы делаете что-то вроде того, что обычно называют "бесконечной прокруткой", вы уменьшаете начальное количество DOM несколько, но оно быстро раздувается после того, как пара обновляется, потому что все эти новые элементы просто привязаны внизу. Прокрутка приходит в обход, потому что прокрутка - это все о количестве элементов. В этом нет ничего бесконечного.

В то время как подход collectionRepeat состоит в том, чтобы использовать только столько элементов, которые будут вписываться в видовое окно, а затем перерабатывать их. Поскольку один элемент вращается вне поля зрения, он отсоединяется от дерева рендеринга, заполняется данными для нового элемента в списке, а затем привязывается к дереву рендеринга на другом конце списка. Это самый быстрый способ, с помощью которого человек получает новую информацию в DOM и из нее, используя ограниченный набор существующих элементов, а не традиционный цикл создания/уничтожения... create/destroy. Используя этот подход, вы действительно можете реализовать бесконечный прокрутки.

Обратите внимание, что вам не нужно использовать Ionic для использования /hack/adapt collectionRepeat или любого другого подобного инструмента. Вот почему они называют это открытым исходным кодом.:-) (Тем не менее, команда Ionic делает несколько гениальных вещей, достойных вашего внимания.)


Там, по крайней мере, один отличный пример делает что-то очень похожее в React. Только вместо того, чтобы перерабатывать элементы с обновленным контентом, вы просто не хотите ничего визуализировать в дереве, которое не отображается. Он быстро вспыхивает на 5000 предметов, хотя их очень простая реализация POC позволяет немного мерцать...


Также... чтобы повторить некоторые другие сообщения, использование track by очень полезно, даже с меньшими наборами данных. Считайте это обязательным.

Ответ 3

Я рекомендую посмотреть следующее:

Оптимизация AngularJS: от 1200 мс до 35 мс

они создали новую директиву, оптимизировав ng-repeat на 4 части:

Оптимизация # 1: элементы DOM кэша

Оптимизация # 2: Совокупные наблюдатели

Оптимизация # 3: создание элементов отложенного элемента

Оптимизация # 4: Наблюдатели за байтами для скрытых элементов

проект находится здесь на github:

Использование:

1- включить эти файлы в одностраничное приложение:

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

2- добавить зависимость модуля:

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

3- заменить ng-repeat

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

Enjoy!

Ответ 4

Помимо всех вышеперечисленных намеков, таких как трек и петли меньшего размера, этот тоже очень помог мне

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

этот фрагмент кода напечатает имя после его загрузки и перестанет смотреть его после этого. Аналогично, для ng-повторов его можно использовать как

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

однако он работает только для версии AngularJS версии 1.3 и выше. Из http://www.befundoo.com/blog/optimizing-ng-repeat-in-angularjs/

Ответ 7

Виртуальная прокрутка - еще один способ улучшить производительность прокрутки при работе с огромными списками. Один из способов реализовать это - использовать Angular Материал md-virtual-repeat.

Взято прямо из документации виртуального повтора:

Виртуальный повтор - это ограниченная замена для ng-repeat, которая отображает только достаточные dom-узлы для заполнения контейнера и их повторного использования по мере прокрутки пользователя.

Демо можно найти здесь

Ответ 8

Правило № 1: Никогда не позволяйте пользователю ждать чего-либо.

Это означает, что растущая страница, которая нуждается в десятисекундах, выглядит быстрее, чем ждать 3 секунды перед пустым экраном и получить все сразу.

Поэтому вместо сделать страницу быстро, просто пусть страница появится быстро, даже если конечный результат медленнее:

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

Приведенный выше код показывает, что список растет по очереди, и он всегда медленнее, чем рендеринг. Но для пользователя он работает быстрее.

Ответ 9

Другая версия @Steffomio

Вместо добавления каждого отдельного элемента мы можем добавлять элементы кусками.

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

Ответ 10

Иногда, что случилось, вы получаете данные с сервера (или back-end) за несколько мс (например, я принимаю это 100 мс), но требуется больше времени для на нашей веб-странице (предположим, что он отображает 900 мс).

Итак, то, что происходит здесь, составляет 800 мс. Это просто для рендеринга веб-страницы.

Что я сделал в своем веб-приложении, я использовал pagination (или вы можете использовать бесконечную прокрутку) для отображения списка данных. Скажем, я показываю 50 данных/страницу.

Поэтому я не буду загружать все данные сразу, изначально загружаются только 50 данных, которые занимают только 50 мс (я предполагаю здесь).

поэтому общее время здесь уменьшилось с 900 мс до 150 мс, после того, как пользователь запросит следующую страницу, затем отобразит следующие 50 данных и так далее.

Надеюсь, это поможет вам улучшить производительность. Все лучшее

Ответ 11

Created a directive (ng-repeat with lazy loading) 

который загружает данные, когда он достигает нижней части страницы, и удаляет половину ранее загруженных данных, и когда он достигает вершины div, снова загружаются предыдущие данные (в зависимости от номера страницы), удаляя половину текущих данных Таким образом, на DOM одновременно присутствуют только ограниченные данные, что может привести к повышению производительности вместо того, чтобы передавать целые данные при загрузке.

КОД 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>

Angular КОД:

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

Демо с директивой

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

В зависимости от высоты раздела он загружает данные, а при прокрутке будут добавлены новые данные, а предыдущие данные будут удалены.

Код 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>

Angular Код:

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;

    }
}

Демо с сетью пользовательского интерфейса с демоном с бесконечным прокруткой

Ответ 12

для большого набора данных и уменьшения нескольких значений лучше использовать ng-options, а не ng-repeat.

ng-repeat медленный, потому что он перебирает все ближайшие значения, но ng-options просто отображает выбор.

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

намного быстрее, чем

<option ng-repeat="state in States" value="{{state.StateCode}}">
    {{state.StateName }}
</option>