168

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

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

另一种选择是将服务器上的数据拆分为页面并在多个请求中获取它们,但这更难看。

我查看了 Angular 文档以寻找类似的ng-repeat="data in dataset" ng-repeat-steps="500"内容,但一无所获。我对 Angular 方式相当陌生,所以我可能完全错过了这一点。这方面的最佳做法是什么?

4

12 回答 12

161

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

使用 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

这种方法对手机来说可能是个问题,因为它们通常在滚动大量数据时会滞后,所以在这种情况下,我认为分页更适合。

为此,您将需要 limitTo 过滤器和自定义过滤器来定义正在显示的数据的起点。

这是一个带有分页的JSBin 。

于 2013-06-27T18:42:28.593 回答
42

Ionic 的 collectionRepeat 指令和其他类似实现的方法体现了用大型数据集克服这些挑战的最热门(可以说是最具可扩展性)的方法。一个花哨的术语是“遮挡剔除”,但您可以将其总结为:不要只将渲染的 DOM 元素的数量限制为任意(但仍然很高)分页数,如 50、100、500... 而是,仅限于用户可以看到的尽可能多的元素

如果您执行通常称为“无限滚动”的操作,您会在一定程度上减少初始DOM 数量,但在几次刷新后它会迅速膨胀,因为所有这些新元素都只是添加在底部。滚动是一种爬行,因为滚动是关于元素数量的。没有什么是无限的。

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

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


至少有一个很好的例子可以在 React 中做一些非常相似的事情。只是不是回收具有更新内容的元素,您只是选择不渲染树中不在视图中的任何内容。它在 5000 个项目上的速度非常快,尽管它们非常简单的 POC 实现允许有点闪烁......


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

于 2015-06-12T07:35:32.620 回答
37

我建议看这个:

优化 AngularJS:1200 毫秒到 35 毫秒

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

优化#1:缓存 DOM 元素

优化#2:聚合观察者

优化#3:延迟元素创建

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

该项目在 github 上:

用法:

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

  • 核心.js
  • scalyr.js
  • slyEvaluate.js
  • slyRepeat.js

2-添加模块依赖:

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

3-替换 ng-repeat

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

请享用!

于 2015-05-31T17:23:26.430 回答
16

除了上述所有提示(如 track by 和更小的循环)之外,这个也对我有很大帮助

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

于 2015-11-27T09:45:11.993 回答
12

您可以使用“track by”来提高性能:

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

比...快:

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

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

于 2015-05-01T14:05:32.913 回答
11

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

这个演示看起来很有前途(它支持惯性滚动)

于 2014-04-20T00:44:45.647 回答
10

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

实现这一点的一种方法是使用Angular Material md-virtual-repeat,因为它在这个Demo 中展示了 50,000 个项目

直接取自 virtual repeat 的文档:

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

于 2016-04-15T16:30:31.650 回答
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
    }
}

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

于 2016-04-20T16:03:03.553 回答
7

另一个版本@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);
    }       
});
于 2016-07-11T23:28:05.647 回答
0

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

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

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

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

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

希望这将帮助您提高性能。祝一切顺利

于 2017-02-08T11:53:16.683 回答
0
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="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>

角代码:

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

角代码:

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 网格演示

于 2017-09-08T02:58:41.420 回答
-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>
于 2016-05-16T08:51:35.580 回答