私はそれぞれ約10フィールド、約2MBのデータを持つ数千行の巨大なデータセットを持っています。ブラウザで表示する必要があります。最も直接的なアプローチ(データを取得し、$scope
に入力し、ng-repeat=""
に任せる)はうまく機能しますが、DOMにノードを挿入し始めると約30分ブラウザがフリーズします。どうやってこの問題に取り組むべきですか?
1つの選択肢は、行を$scope
に増分的に追加し、ngRepeat
が1つのチャンクのDOMへの挿入を完了するのを待ってから次のチャンクに移動することです。しかし、AFAIK ngRepeatは "繰り返し"が終了しても報告しないため、見苦しいものになるでしょう。
他の選択肢は、サーバー上のデータをページに分割し、それらを複数の要求でフェッチすることですが、それはさらに厄介です。
ng-repeat="data in dataset" ng-repeat-steps="500"
のようなものを探すためにAngularのドキュメントを調べましたが、何も見つかりませんでした。私はAngularのやり方にかなり慣れていないので、その点を完全に見逃している可能性があります。これでベストプラクティスは何ですか?
@ AndreM96に同意します。最善のアプローチは限られた量の行だけを表示することです(より速くより良いUX)。これはページ付けまたは無限スクロールで行うことができます。
Angularを使用した無限スクロールは、 limitTo filterを使用すると非常に簡単です。初期制限を設定するだけでよく、ユーザーがより多くのデータを要求したとき(単純にするためにボタンを使用しています)は、制限を増やします。
<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数をいくらか減らすことになりますが、2、3回更新するとすぐに大きくなります。なぜなら、これらすべての新しい要素は、一番下に追加されているだけだからです。スクロールはすべて要素数に関するものであるため、スクロールはクロールになります。それについて無限は何もありません。
一方、collectionRepeat
のアプローチでは、ビューポートに収まるだけの要素を使用してから、それらをリサイクルします。一方の要素が表示外に回転すると、その要素はレンダーツリーから切り離され、リスト内の新しい項目のデータが再入力されてから、リストのもう一方の端でレンダーツリーに再アタッチされます。これは、create/destroy ... create/destroyの伝統的なサイクルではなく、既存の要素の限られたセットを利用して、DOMに出入りする新しい情報を取得するために人間が知る最も速い方法です。このアプローチを使うと、本当に無限スクロールを実装することができます。
/ hack/adapt collectionRepeat
やそれに類するものを使うのにIonicを使う必要はないことに注意してください。だからこそ、彼らはそれをオープンソースと呼んでいます。 :-)(とは言っても、Ionicチームはかなり独創的なことをしています。あなたの注意に値します。)
Reactで非常によく似たことをする少なくとも1つの 優れた例 があります。更新されたコンテンツで要素を再利用するのではなく、表示されていないものをツリーにレンダリングしないことを選択しているだけです。非常に単純なPOCの実装ではちょっとしたちらつきが許されていますが、5000項目が非常に高速です。
また、他の投稿の一部をエコーするために、track by
を使用することは、小さいデータセットでも非常に役に立ちます。必須と考えてください。
私はこれを見ることをお勧めします:
AngularJSの最適化:1200ミリ秒から35ミリ秒
彼らは4つの部分でng-repeatを最適化することによって新しいディレクティブを作りました:
最適化1:DOM要素をキャッシュする
最適化#2:集約ウォッチャー
最適化#3:要素作成を遅らせる
最適化#4:隠れた要素に対してウォッチャーをバイパスする
プロジェクトはgithubにあります。
1 - あなたのシングルページアプリにこれらのファイルを含めます。
2-モジュールの依存関係を追加します。
var app = angular.module("app", ['sly']);
3- ng-repeatを置き換えます
<tr sly-repeat="m in rows"> .....<tr>
楽しい!
Track byや小さいループのような上記のヒントすべてに加えて、これも私を大きく助けてくれました。
<span ng-bind="::stock.name"></span>
このコードはロードされると名前を表示し、その後は監視を中止します。同様に、ng-repeatについては、
<div ng-repeat="stock in ::ctrl.stocks">{{::stock.name}}</div>
ただし、AngularJSバージョン1.3以上でのみ機能します。から http://www.befundoo.com/blog/optimizing-ng-repeat-in-angularjs/
パフォーマンスを向上させるために "track by"を使うことができます。
<div ng-repeat="a in arr track by a.trackingKey">
より速い:
<div ng-repeat="a in arr">
ref: https://www.airpair.com/angularjs/post/Angularjs-performance-large-applications
すべての行の高さが同じ場合は、仮想化のng-repeatを必ず見てください。 http://kamilkp.github.io/angular-vs-repeat/
これ demo は非常に有望に見えます(そして慣性スクロールをサポートします)。
規則No.1:ユーザーに何も待たせない。
それを念頭に置いておくと、空白の画面が表示される前に3秒間待機して一度にすべてを取得するよりも、10秒を要する人生を伸ばすページがはるかに早く表示されます。
したがって、makeページを速くする代わりに、最終結果が遅くなっても表示を速くするだけで構いません。
function applyItemlist(items){
var item = items.shift();
if(item){
$timeout(function(){
$scope.items.Push(item);
applyItemlist(items);
}, 0); // <-- try a little gap of 10ms
}
}
上記のコードは、リストが行ごとに大きくなっているように見せていますが、一度にすべてをレンダリングするよりも常に遅くなります。 しかしユーザーにとってはそれは速いようです。
仮想スクロールは、巨大なリストや大きなデータセットを扱うときのスクロールパフォーマンスを向上させるもう1つの方法です。
これを実装する1つの方法は、 Angular Materialmd-virtual-repeat
を使用することです。これはこのデモで説明されているとおりです 50,000項目のデモ
バーチャルリピートのドキュメンテーションから直接抜粋しました:
バーチャルリピートはng-repeatの限定的な代用品で、コンテナをいっぱいにしてユーザーがスクロールしたときにそれらをリサイクルするのに十分なドムノードだけをレンダリングします。
他のバージョン@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);
}
});
Created a directive (ng-repeat with lazy loading)
これは、ページの最後に到達したときにデータをロードし、以前にロードされたデータの半分を削除し、以前のデータを再びdivのトップに到達したときにロードします。一度に限られたデータしか存在しないため、データ全体をロード時にレンダリングする代わりにパフォーマンスが向上する可能性があります。
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)\"> </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;
}
}
時々起こったこと、あなたは数msでサーバー(またはバックエンド)からデータを取得します(例えば100msと仮定しています)しかし表示にはもっと時間がかかります私たちのWebページ(表示するのに900msかかっているとしましょう)。
だから、ここで何が起こっているのは800msですそれは単にWebページをレンダリングするために取っています。
私のWebアプリケーションで行ったことは、ページ付け(または無限スクロールも使用できます)を使用して表示したことです。データのリスト1ページあたり50データを表示しているとしましょう。
そのため、すべてのデータを一度にロードレンダリングすることはしません。最初にロードするデータは50データのみで、50ミリ秒しかかかりません(ここでは想定しています)。
そのため、ここでの合計時間は900ミリ秒から150ミリ秒に短縮されました。ユーザーが次のページを要求すると、次の50のデータが表示されます。
これがあなたがパフォーマンスを向上させるのに役立つことを願っています。すべての最高