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



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



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



другой вариант-разделить данные на сервере на страницы и получить их в нескольких запросах, но это еще уродливее.



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

586   12  

12 ответов:

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

Бесконечная прокрутка с угловым действительно проста с 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.

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

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

здесь JSBin с разбиением на страницы.

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

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

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

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


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


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

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

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

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

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

оптимизация#2: Aggregate watchers

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

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

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

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

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

  • ядра.js
  • scalyr.js
  • slyEvaluate.js
  • slyRepeat.js

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

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

3- замена НГ-повтор

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

наслаждайтесь!

помимо всех вышеперечисленных подсказок, таких как track by и smaller loops, этот также очень помог мне

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

Если все ваши строки имеют одинаковую высоту, вы обязательно должны взглянуть на виртуализацию ng-repeat:http://kamilkp.github.io/angular-vs-repeat/

этой demo выглядит очень перспективно (и поддерживает инерционную прокрутку)

вы можете использовать "track by", чтобы увеличить производительность:

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

быстрее:

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

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

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

один из способов реализовать это с помощью Угловое Материалmd-virtual-repeat Как это показано на этом демо с 50 000 элементов

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

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

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

что в виду жизни растущая страница, которая нуждается в 10 секунд появляется намного быстрее, чем ждать 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
    }
}

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

другая версия @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);
    }       
});

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

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

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

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

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

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

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>

Угловое код:

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>

Угловое Код:

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;

    }
}

демо с UI сеткой с бесконечной прокруткой демо

для большого набора данных и нескольких выпадающих значений лучше использовать 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>

Comments

    Ничего не найдено.