如何提高ng的性能重复一个庞大的数据集(angularjs)?

时间:2013-06-27 16:07:12

标签: javascript angularjs angularjs-ng-repeat

我有一个数千行的庞大数据集,每行约10个字段,大约2MB的数据。我需要在浏览器中显示它。最简单的方法(获取数据,将其放入$scope,让ng-repeat=""完成其工作)工作正常,但当它开始将节点插入DOM时,它会冻结浏览器大约半分钟。我该如何处理这个问题?

一个选项是逐行将行追加到$scope并等待ngRepeat完成将一个块插入DOM,然后再移动到下一个。但AFAIK ngRepeat在完成“重复”时不报告,所以它会变得丑陋。

其他选择是将服务器上的数据拆分成页面并在多个请求中获取它们,但这甚至更加丑陋。

我查看了Angular文档,搜索了ng-repeat="data in dataset" ng-repeat-steps="500"之类的内容,但一无所获。我对Angular方式相当新,所以我可能完全忽略了这一点。这方面的最佳做法是什么?

12 个答案:

答案 0 :(得分:155)

我同意@ AndreM96的最佳方法是只显示有限数量的行,更快更好的UX,这可以通过分页或无限滚动来完成。

使用limitTo过滤器,使用Angular的无限滚动非常简单。您只需设置初始限制,当用户要求更多数据时(为简单起见,我使用的是按钮),您可以增加限制。

<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

答案 1 :(得分:38)

使用大型数据集克服这些挑战的最热门 - 也可称最具扩展性 - 的方法体现在Ionic's collectionRepeat directive和其他类似实现的方法中。一个奇特的术语是'occlusion culling',但你可以总结为:不要只是将渲染的DOM元素的数量限制为任意(但仍然很高)的分页数,如50,100,500 ......相反,仅限于用户可以看到的元素数量

如果您执行的操作通常称为“无限滚动”,那么您在某种程度上会减少初始 DOM计数,但在一次刷新后它会很快膨胀,因为所有这些新元素都只是坚持到底。滚动是一种爬行,因为滚动是关于元素计数的。它没有什么无限的。

然而,collectionRepeat方法仅使用适合视口的元素,然后回收它们。当一个元素旋转出视图时,它将从渲染树中分离,重新填充列表中新项目的数据,然后重新附加到列表另一端的渲染树。这是人类已知的最快的方式来获取进出DOM的新信息,利用一组有限的现有元素,而不是传统的创建/销毁循环...创建/销毁。使用这种方法,您可以真正实现无限滚动。

请注意,您不必使用Ionic来使用/ hack / adapt collectionRepeat或任何其他类似的工具。这就是他们称之为开源的原因。 :-)(那就是说,Ionic团队正在做一些非常巧妙的事情,值得你注意。)

在React中至少有一个excellent example做了非常相似的事情。只是选择不在树中渲染任何不在视图中的内容,而不是使用更新的内容回收元素。虽然它们非常简单的POC实现允许闪烁一点,但它在5000项上的速度非常快......

另外......为了回应其他一些帖子,使用track by非常有帮助,即使数据集较小也是如此。认为它是强制性的。

答案 2 :(得分:35)

我建议看到这个:

优化AngularJS:1200毫秒至35毫秒

他们通过优化4个部分的ng-repeat来制定新的指令:

  

优化#1:缓存DOM元素

     

优化#2:聚合观察者

     

优化#3:延迟元素创建

     

优化#4:绕过观察者隐藏的元素

项目在github:

用法:

1-将这些文件包含在您的单页应用中:

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

2-添加模块依赖:

.darkHeader

3-替换ng-repeat

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

享受!

答案 3 :(得分:14)

除了上述所有提示,例如track by和small loops之外,这个提示也帮了我很多次

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

这段代码会在加载后打印出来,并在此之后停止观看。同样,对于ng-repeats,它可以用作

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

然而它仅适用于AngularJS 1.3及更高版本。 从 http://www.befundoo.com/blog/optimizing-ng-repeat-in-angularjs/

答案 4 :(得分:11)

您可以使用&#34;跟踪&#34;提高绩效:

<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

答案 5 :(得分:10)

如果你的所有行都有相同的高度,你一定要看看虚拟化ng-repeat:http://kamilkp.github.io/angular-vs-repeat/

demo看起来非常有前景(它支持惯性滚动)

答案 6 :(得分:9)

虚拟滚动是在处理大型列表和大型数据集时提高滚动性能的另一种方法。

实现此目的的一种方法是使用Angular Material md-virtual-repeat,因为它已在此Demo with 50,000 items上展示

直接从虚拟重复的文档中获取:

  

虚拟重复是ng-repeat的有限替代品,它只渲染足够的dom节点来填充容器并在用户滚动时回收它们。

答案 7 :(得分:9)

规则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
    }
}

上面的代码让列表逐行增长,并且总是慢于一次渲染。 但对于用户来说似乎更快。

答案 8 :(得分:5)

Another version @Steffomio

Instead of adding each item individually we can add items by chunks.

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

答案 9 :(得分:0)

有时会发生什么,您可以在几毫秒内从服务器(或后端)获取数据(例如,我假设它是100毫秒)但是需要更多时间显示在我们的网页(假设显示需要900毫秒)。

所以,这里发生的事情是800毫秒这只是为了渲染网页。

我在网络应用程序中所做的是,我使用了分页(或者您也可以使用无限滚动)来显示数据列表。假设我显示50个数据/页。

所以我不会一次加载渲染所有数据,最初只加载50个数据,只需要50ms(我假设在这里)。

所以这里的总时间从900ms减少到150ms,一旦用户请求下一页然后显示接下来的50个数据,依此类推。

希望这可以帮助您提高性能。一切顺利

答案 10 :(得分:0)

Created a directive (ng-repeat with lazy loading) 

当数据到达页面底部时加载数据并删除一半以前加载的数据,当它再次到达div的顶部时,将加载先前的数据(取决于页码),删除一半的当前数据所以在DOM上一次只能存在有限的数据,这可能会带来更好的性能,而不是在加载时渲染整个数据。

HTML CODE:

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

Angular CODE:

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

Demo with directive

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="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>

Angular Code:

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;

    }
}

Demo with UI grid with infinite-scroll Demo

答案 11 :(得分:-2)

对于大数据集和多值下拉,最好使用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>