如何在一个巨大的数据集(angular.js)上提高 ngRepeat 的性能?

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

一种选择是以增量的方式将行附加到 $scope,并等待 ngRepeat完成将一个块插入到 DOM 中,然后再移动到下一个块。但是 AFAIK ng 重复在完成“重复”后不会报告,所以它会很难看。

另一种选择是将服务器上的数据分割成多个页面,并在多个请求中获取它们,但这种做法更为糟糕。

我浏览了 Angular 的文档,寻找类似 ng-repeat="data in dataset" ng-repeat-steps="500"的东西,但是一无所获。我是相当新的角度的方式,所以有可能我是完全错过了这一点。这方面的最佳实践是什么?

143281 次浏览

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

无限旋转角度是真正简单的 限制滤波器。您只需设置初始限制,当用户要求更多数据时(为了简单起见,我使用了一个按钮) ,您就可以增加这个限制。

<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 To 过滤器和一个自定义过滤器来定义要显示的数据的起始点。

这是一个带有分页的 JSBin

如果所有行都具有相同的高度,那么您肯定应该查看一下虚拟化 ng-repeat: http://kamilkp.github.io/angular-vs-repeat/

这个 小样看起来非常有前途(它支持惯性滚动)

你可以使用“ 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

我建议看看这个:

优化 AngularJS: 1200ms 到35ms

他们通过在四个部分优化 n- 重复来制定一个新的指令:

优化 # 1: 缓存 DOM 元素

优化 # 2: 聚合观察者

优化 # 3: 推迟元素创建

优化 # 4: 隐藏元素的旁路监视器

这个项目在 github 上:

用法:

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

  • CoreJs
  • Scalyr.js
  • Slyevalate.js
  • Slyrepeat. js

2-add 模块依赖性:

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

3-替换 n- 重复

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

享受吧!

使用大型数据集克服这些挑战的最热门——也可以说是最具可伸缩性的——方法体现在 爱奥尼亚集合,重复指令和其他类似实现的方法中。对于这种情况,一个花哨的术语是 “遮挡剔除”,但是您可以将其总结为: 不要仅仅将呈现的 DOM 元素的计数限制为任意(但仍然很高)的分页数字,比如50、100、500... 而是 仅限于用户可以看到的尽可能多的元素

如果你做一些通常被称为“无限滚动”的事情,你会减少 首字母缩写 DOM 的数量,但是刷新几次之后,它会迅速膨胀,因为所有这些新元素都是在底部添加的。滚动变成了爬行,因为滚动完全是关于元素计数的。没有什么是无限的。

然而,collectionRepeat的方法是只使用将在 viewport 中容纳的尽可能多的元素,然后使用 回收利用。当一个元素从视图中旋转出来时,它将从呈现树中分离出来,用列表中新项的数据重新填充,然后重新附加到列表另一端的呈现树中。这是人类已知的获取新信息进出 DOM 的最快方法,利用有限的现有元素集,而不是传统的创建/破坏... 创建/破坏的循环。使用这种方法,您可以真正实现 无穷无尽滚动。

请注意,您不必使用爱奥尼亚使用/hack/适应 collectionRepeat,或任何其他类似的工具。这就是为什么他们称之为开源。(也就是说,爱奥尼亚团队正在做一些相当巧妙的事情,值得你们关注。)


在 React 中至少有一个 很好的例子在做类似的事情。只不过不是使用更新后的内容回收元素,而是简单地选择不呈现树中不在视图中的任何内容。它在5000个项目上的速度非常快,尽管它们非常简单的 POC 实现允许一点闪烁..。


另外,为了回应其他一些文章,使用 track by是非常有帮助的,即使是对于较小的数据集。考虑它的强制性。

除了以上所有的提示,如跟踪和较小的循环,这一个也帮了我很多

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

这段代码将打印的名称一旦加载,并停止观看后,它。类似地,对于 n- 重复,它可以用作

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

但它只适用于 AngularJS 1.3或更高版本。 从 Http://www.befundoo.com/blog/optimizing-ng-repeat-in-angularjs/

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

实现这一点的一种方法是使用 角状物质 md-virtual-repeat,因为它是在这个 有五万件物品的演示上演示的

直接来自虚拟重复的文档:

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

上面的代码让列表一行一行地增长,并且总是比一次全部呈现慢。它似乎更快。

对于大数据集和多值下降的情况,最好使用 ng-options而不是 ng-repeat

ng-repeat很慢,因为它循环遍历所有接下来的值,但是 ng-options只是显示到 select 选项。

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

远远快于

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

另一个版本@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毫秒,它只需要渲染网页。

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

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

所以这里的总时间从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="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;


}
}

无限滚动界面网格演示