原文在此:AngularJS Performance Tuning for Long Lists
警告:此篇為翻譯文章,所有內容皆為原作者所有
此外歡迎指正破英文,如果哪邊有嚴重謬誤哪肯定是我把靈魂出賣給了 google 小姐
AngularJS 肥 Lists 效能調教
AnglarJS
棒棒!但是當他在處裡包含複雜 data 結構的肥 Lists的時候,會像駭客任務一樣產生子彈時間般的世界都靜止了,我們在將核心移植到AnglarJS的時候遭遇這個鳥問題。照理來說應該要很滑溜的顯示500 rows,但是第一次親密接觸的時候花了七秒鐘才顯示出來,這下悲劇惹!
我們發現實作上兩項主要的效能問題,一個是關於ng-repeat directive,另一個是關於filtering。
接下來的文章記錄了我們使用各種伎倆,來解決或減輕親密接觸時感受不好的經驗談,這會給你一些點子跟提示,讓你知道什麼東東你可以自個兒玩玩,什麼東西最好不要亂玩。
為神馬ng-repeat 碰上肥lists會這麼慢?
AngularJS
的 ng-repeat directive 大約在 2500 個two-way data binding 的時候就快升天了。你可以看看Misko
Hevery 的文章(See further reading no 2)。這是因為 AngularJS 使用髒檢查(dirty checking)來監聽所有改變,每次的監聽都要消耗時間,所以包含複雜 data 結構的肥 Lists 將會搞爛你的 APP。
先來瞧瞧效能發生啥事
Time logging
directive:
我們寫了個簡單的directive來測量list 渲染出來需要花多少時間,他會記錄ng-repeat生出$last浪費多少光陰,資料的參考會存在TimeTracker-service裡面,所以他跟從server讀取data的時間是獨立開來紀錄的。
// Post repeat directive for
logging the rendering time
angular.module('siApp.services').directive('postRepeatDirective',
['$timeout', '$log', 'TimeTracker',
function ($timeout, $log, TimeTracker) {
return function (scope, element, attrs) {
if (scope.$last) {
$timeout(function () {
var timeFinishedLoadingList =
TimeTracker.reviewListLoaded();
var ref = new Date(timeFinishedLoadingList);
var end = new Date();
$log.debug("## DOM
rendering list took: " + (end - ref) + " ms");
});
}
};
}
]);
//Use in HTML:
//<tr
ng-repeat="item in items" post-repeat-directive>...</tr>
Chrome 的時間軸開發者工具
在 Chrome 開發者工具 timeline 的 tab,你可以看到事件和瀏覽器每秒記憶體的分配。記憶體工具對於偵查記憶體消耗和一頁需要多少記憶體十分有效。每秒幀數低於30是頁面閃爍是大部分的問題點,幀數工具提供了渲染效能的觀察,此外他還顯示
JavaScript 消耗了多少 CPU 的時間。
最基本的調教伎倆 - 限制 List 的Size
限制顯示時 List 的 Size 是舒緩問題的最佳的方式,你可以玩玩分頁,或者無限捲捲。
分頁
我們使用 AngularJS 玩分頁是靠”limitTo” filter (1.1.4才有 :P) 搭配自製”startFrom”filter。它可以讓我們限制List 的 Size 來減少渲染時間,他是最有效減少渲染時間的方法。
如果你不能或者不想這麼幹,但是你有filtering慢的症狀,看看步驟五,然後使用ng-show 來隱藏要過濾掉的元素。
//Pagination in controller
$scope.currentPage = 0;
$scope.pageSize = 75;
$scope.numberOfPages = function() {
return Math.ceil($scope.displayedItemsList.length/
$scope.pageSize);
};
//Start from filter
angular.module('app').filter('startFrom', function() {
return function(input, start) {
return input.slice(start);
};
//Use in HTML
//Pagination buttons
//<button ng-repeat="i
in getNumber(numberOfPages()) track by $index"
ng-click="setCurrentPage($index)">{{$index + 1}}</button
//Displayed list
//<tr
ng-repeat="item in displayedItemsList | startFrom: currentPage *
pageSize | limitTo:pageSize"
/tr>
無限捲捲
使用無限捲捲不是我們的使用情境的選項,如果你想深入的了解一下無限捲捲,下面這個連結將帶領你前往AngularJS infinite scrolling 專案:
調教方針
1.渲染List的時候不要使用data binding
這個是最顯而易見的解法,data-binding最有可能是效能問題的起點。拋棄data binding是絕對沒問題的,如果你只是要一次性的顯示這些資料,而且不需要處理更新或變化。悲劇的是你的List將失去控制,所以這個親密接觸的方法也不是我們的選項。
有興趣點這個:
2. 不要使用行內方法呼叫計算data
為了在controller裡面直接過濾List,不要使用方法來取得過濾後的集合,ng-repeat會在每次$diges時計算每個expression,這將會十分頻繁。在我們的範例裡filteredItems() 返回濾後的集合,如果這個evaluation 很慢,這將會立馬拖慢APP速度。
<li ng-repeat="item in
filteredItems()"> <!--Bad idea, since very often evaluated.-->
<li ng-repeat="item in items"> <!--Way to go! -->
3. 用兩個List (一個給view 顯示,一個給data)
區分開顯示用的List跟資料用的List是實用的模式,這讓你可以偷偷的玩一些filters 和快取住集合再交給 view。下面是一個非常基本的範例,filteredLists 變數用來接住集合,applyFilter 方法負責處理mapping.
/* Controller */
// Basic list
var items = [{name:"John", active:true }, {name:"Adam"}, {name:"Chris"}, {name:"Heather"}];
// Init displayedList
$scope.displayedItems = items;
// Filter Cache
var filteredLists['active'] = $filter('filter')(items, {"active" : true});
// Apply the filter
$scope.applyFilter = function(type) {
if (filteredLists.hasOwnProperty(type)){ // Check if filter is cached
$scope.displayedItems =
filteredLists[type];
} else {
/* Non cached filtering */
}
}
// Reset filter
$scope.resetFilter = function() {
$scope.displayedItems = items;
}
/* View */
//<button
ng-click="applyFilter('active')">Select active</button>
//<ul><li
ng-repeat="item in
displayedItems">{{item.name}}<li></ul>
4.使用ng-if 代替 ng-show顯示額外的樣板
如果你的directives 或 templates 需要秀一些額外的資訊,例如點list item 的後顯示,你最好使用 ng-if(1.1.5才有:P),ng-if 不給渲染(跟ng-show比較起來)。因此額外的DOM
elements 和data-bindings 將會在需要的時候才計算。
<li ng-repeat="item in items">
<p>{{ item.title }} </p>
<button ng-click="item.showDetails
= !item.showDetails">Show details</buttons>
<div ng-if="item.showDetails">
{{item.details}}
</div>
</li>
5. 不要使用ng-mouseenter,
ng-mouseleave, 之類的東東
內建的 ng-mouseenter 會造成閃爍,主要是因為瀏覽器低於每秒30幀,使用正港的jQuery來造動畫和滑過效果來解決這個問題吧。如果要注意最後的DOM改變狀態,記得要使用jQuery 的live() function包裝mouse 事件。
6. 調教filtering的小提示: 使用ng-show隱藏要排除的元素
對肥List 使用filters 同樣令人髮指。因為每個 filter 會對原本的 List 產生一個子集合,在許多情況下,當 data 沒有改變的情況下 filter 返回相同的東西,因此預先過濾 data 再套用到view 能省不少時間。
在ng-repeat中套用filters的時候,每個filter回傳原集合的子集,AngularJS會在$destroy的時候移除排除掉的DOM元素,也會移除他們的 $scope。當filter 輸入改變,子集會跟著改變,然後元素必須被重新連結或者再次$destroyed。在大多數情況下,這是絕對沒問題的,但是當使用者頻繁的使用filters,或者List 非常肥,這會持續連結和銷毀DOM元素而影響效能。要加快過濾的話你可以使用ng-show 和ng-hide directives。在controller裡面計算和賦予旗標給每個需要過濾的項目,根據旗標觸發ng-show,因此他只會增加class 給elements 代替移除子集、$scope 和DOM。
1.
一個觸發 ng-show 的方法是使用表達式語法,ng-show 的值是通過filter 語法計算出來。
可以參閱這兒 plunkr
example
<input ng-model="query"></input>
<li ng-repeat="item in items" ng-show="([item.name] |
filter:query).length">{{item.name}}</li>
2.
另一個方法是使用attribute 傳遞ng-show 然後用另一個子controller來計算,這是種比較複雜的方式,然而Ben Nadel有更妙的的建議在他的
blog
post
7. 調教filtering的小提示: Debounce input
延續第六點,另一個解決連續filtering的方法是防止input抖動。舉例來說,如果使用者KEY了一些關鍵字,filter只需要在停止輸入之後再開始作用即可。
一個防抖動的好解法 - debounce service
套用在你的view 跟controller 像這樣:
/* Controller */
// Watch the queryInput and
debounce the filtering by 350 ms.
$scope.$watch('queryInput', function (newValue, oldValue) {
if (newValue === oldValue) { return; }
$debounce(applyQuery, 350);
});
var applyQuery = function () {
$scope.filter.query = $scope.query;
};
/* View */
//<input
ng-model="queryInput"/>
//<li
ng-repeat= item in items | filter:filter.query>{{ item.title }} </li>
Further reading
§ 1. Project organization with huge apps: http://briantford.com/blog/huuuuuge-angular-apps.html
§ 2. Stackoverflow answer of Misko Hevery concerning angular
data-binding performance: http://stackoverflow.com/questions/9682092/databinding-in-angularjs/9693933#9693933
§ 3. Short article with different approaches increase ng-repeat
performance:http://www.williambrownstreet.net/blog/?p=437
§ 4. Loading more data on request:http://stackoverflow.com/questions/17348058/how-to-improve-performance-of-ngrepeat-over-a-huge-dataset-angular-js
§ 5. Good article on using the scope: http://thenittygritty.co/angularjs-pitfalls-using-scopes
§ 6. AngularJS project for dynamic templates http://onehungrymind.com/angularjs-dynamic-templates
§ Rendering without data-binding: https://github.com/Pasvaz/bindonce