如何提高一个巨大的数据集(angular.js)ngRepeat的性能?
我有一个有数千行的大数据集,每个大约有10个字段,大约2MB的数据。 我需要在浏览器中显示它。 最直接的方法(获取数据,把它放到$scope
,让ng-repeat=""
完成它的工作)可以正常工作,但是当它开始向DOM中插入节点时,它会冻结浏览器大约半分钟。 我应该如何解决这个问题?
一种select是将行追加到$scope
然后等待ngRepeat
完成将一个块插入DOM,然后再移动到下一个块。 但AFAIK ngRepeat在完成“重复”时不会报告,所以会很难看。
其他select是将服务器上的数据分成多个页面,并在多个请求中获取它们,但这更糟糕。
我通过Angular文档查找类似于ng-repeat="data in dataset" ng-repeat-steps="500"
,但没有发现任何内容。 我对angular度方面相当陌生,所以我可能完全错过了这一点。 这方面的最佳做法是什么?
我同意@ AndreM96,最好的办法是只显示有限的行数,更快,更好的UX,这可以通过分页或无限滚动来完成。
无限滚动与Angular是非常简单的与limitTofilter。 你只需要设置初始限制,当用户要求更多的数据时(为了简单起见,我使用了一个button)就增加了限制。
<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 。
这种方法对于手机来说可能是一个问题,因为在滚动大量数据的时候,通常它们会滞后,所以在这种情况下,我认为分页更合适。
要做到这一点,你将需要limitTofilter和一个自定义filter来定义显示的数据的起点。
这是一个JSBin与分页。
用大数据集克服这些挑战的最热门的 – 可以说是最可扩展的 – 方法体现在Ionic的collectionRepeat指令和其他类似的实现方法中。 一个奇特的术语是“遮挡剔除” ,但是你可以总结为:不要只把渲染的DOM元素的数量限制为任意的(但仍然是高)分页数,比如50,100,500 … 只限于用户可以看到的元素 。
如果你做一些像“无限滚动”这样的东西,你可能会减less最初的 DOM计数,但是在两次刷新之后它会迅速膨胀,因为所有这些新的元素都是在底部加上的。 滚动进入爬行,因为滚动是关于元素计数。 没有什么无限的。
然而, collectionRepeat
方法只能使用适合视口的许多元素,然后回收它们 。 当一个元素旋转出视图时,它将从渲染树中分离,重新填充列表中新项目的数据,然后重新连接到列表另一端的渲染树。 这是人们已知的获取DOM信息的最新方法,利用有限的现有元素集,而不是传统的创build/销毁循环创build/销毁。 使用这种方法,您可以真正实现无限滚动。
请注意,您不必使用Ionic来使用/ hack / adapt collectionRepeat
或其他任何类似的工具。 这就是为什么他们称之为开源。 :-)(也就是说,Ionic团队正在做一些非常巧妙的事情,值得你的关注。)
在React中至less有一个非常类似的例子 。 只是不使用更新的内容来回收元素,只是简单地select不渲染树中不可见的东西。 它在5000个项目上快速发展,虽然它们非常简单的POC实现允许一些闪烁…
另外…回声一些其他职位,使用track by
是非常有帮助的,即使是较小的数据集。 考虑它是强制性的。
我build议看看这个:
优化AngularJS:1200ms到35ms
他们通过优化4个部分的ng-repeat来做出新的指示:
优化#1:cachingDOM元素
优化#2:聚合观察者
优化#3:推迟元素创build
优化#4:绕过观察者隐藏的元素
这个项目在github上:
用法:
1-将这些文件包含在您的单页应用程序中:
- core.js
- scalyr.js
- slyEvaluate.js
- slyRepeat.js
2-添加模块依赖项:
var app = angular.module("app", ['sly']);
3-replaceng-repeat
<tr sly-repeat="m in rows"> .....<tr>
请享用!
旁边所有的提示,像循环和小循环,这一个也帮了我很多
<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/
如果你所有的行都有相同的高度,你应该看看虚拟化的ng-repeat: http : //kamilkp.github.io/angular-vs-repeat/
这个演示看起来非常有前途(它支持惯性滚动)
您可以使用“追踪”来提高性能:
<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
虚拟滚动是处理巨大列表时提高滚动性能的另一种方法。 实现这一点的一种方法是使用Angular Material md-virtual-repeat
。
从虚拟重复的文档直接采取:
虚拟重复是ng-repeat的有限替代,它只呈现足够的dom节点来填充容器,并在用户滚动时回收它们。
演示可以在这里find
规则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); } });
有时候发生了什么, 你在几毫秒内从服务器(或后端)获得数据 (例如,我假设它是100ms),但是它需要更多的时间来显示在我们的网页 (假设它需要900ms显示)。
那么,这里发生了什么是800ms它只是呈现网页。
我在web应用程序中所做的是我已经使用了分页 (或者也可以使用无限滚动 )来显示数据列表。 假设我正在显示50个数据/页面。
所以我不会一次加载所有的数据,只有50个数据我最初加载只需要50ms(我假设在这里)。
所以这里的总时间从900ms减less到150ms,一旦用户请求下一页,然后显示下一个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>
angularCODE:
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>
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)\"> </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
只是显示到select选项。
ng-options='state.StateCode as state.StateName for state in States'>
比…快得多
<option ng-repeat="state in States" value="{{state.StateCode}}"> {{state.StateName }} </option>