對(duì)AngularJS進(jìn)行性能調(diào)優(yōu)的7個(gè)建議
AnglarJS作為一款優(yōu)秀的Web框架,可大大簡(jiǎn)化前端開(kāi)發(fā)的負(fù)擔(dān)。近日Sebastian Fröstl在一篇博文《AngularJS Performance Tuning for Long Lists》中表示AnglarJS在處理包含復(fù)雜數(shù)據(jù)結(jié)構(gòu)的大型列表時(shí),其運(yùn)行速度會(huì)非常慢。他在文中同時(shí)分享了解決方案。下面為該文的譯文。
AnglarJS很棒,但當(dāng)處理包含復(fù)雜數(shù)據(jù)結(jié)構(gòu)的大型列表時(shí),其運(yùn)行速度就會(huì)非常慢。這是我們將核心管理頁(yè)面遷移到AngularJS過(guò)程中遇到的問(wèn)題。這些頁(yè)面在顯示500行數(shù)據(jù)時(shí)本應(yīng)該工作順暢,但首個(gè)方法的渲染時(shí)間竟花費(fèi)了7秒,太可怕了。
后來(lái),我們發(fā)現(xiàn)了在實(shí)現(xiàn)過(guò)程中存在兩個(gè)主要性能問(wèn)題。一個(gè)與“ng-repeat ”指令有關(guān),另一個(gè)與過(guò)濾器有關(guān)。
下文將分享我們通過(guò)不同的方法解決性能問(wèn)題的經(jīng)驗(yàn),希望可以給你帶來(lái)啟示。
一、AngularJS 中的ng-repeat在處理大型列表時(shí),速度為什么會(huì)變慢?
AngularJS中的ng-repeat在處理2500個(gè)以上的雙向數(shù)據(jù)綁定時(shí)速度會(huì)變慢。這是由于AngularJS通過(guò)“dirty checking”函數(shù)來(lái)檢測(cè)變化。每次檢測(cè)都會(huì)花費(fèi)時(shí)間,所以包含復(fù)雜數(shù)據(jù)結(jié)構(gòu)的大型列表將降低你應(yīng)用的運(yùn)行速度。
二、提高性能的先決條件
時(shí)間記錄指令
為了測(cè)量一個(gè)列表渲染所花費(fèi)的時(shí)間,我們寫(xiě)了一個(gè)簡(jiǎn)單的程序,通過(guò)使用“ng-repeat”的屬性“$last”來(lái)記錄時(shí)間。時(shí)間存放在TimeTracker服務(wù)中,這樣時(shí)間記錄就與服務(wù)器端的數(shù)據(jù)加載分開(kāi)了。
- // 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開(kāi)發(fā)者工具的時(shí)間軸(Timeline)屬性
在Chrome開(kāi)發(fā)者工具的時(shí)間軸標(biāo)簽中,你可以看見(jiàn)事件、每秒內(nèi)瀏覽器幀數(shù)和內(nèi)存分配。“memory”工具用來(lái)檢測(cè)內(nèi)存泄漏,及頁(yè)面所需的內(nèi) 存。當(dāng)幀速率每秒低于30幀時(shí)就會(huì)出現(xiàn)頁(yè)面閃爍問(wèn)題。“frames”工具可幫助了解渲染性能,還可顯示出一個(gè)JavaScript任務(wù)所花費(fèi)的CPU時(shí) 間。
三、通過(guò)限制列表的大小進(jìn)行基本的調(diào)優(yōu)
緩解該問(wèn)題,最好的辦法是限制所顯示列表的大小??赏ㄟ^(guò)分頁(yè)、添加無(wú)限滾動(dòng)條來(lái)實(shí)現(xiàn)。
分頁(yè)
分頁(yè),我們可以使用AngularJS的“limitTo”過(guò)濾器(AngularJS1.1.4版本以后)和“startFrom”過(guò)濾器??梢酝ㄟ^(guò)限制顯示列表的大小來(lái)減少渲染時(shí)間。這是減少渲染時(shí)間最高效的方法。
- // 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>
如果你不能/不想使用分頁(yè),但過(guò)濾過(guò)程又很慢,這時(shí)一定要檢查前五步,并使用“ng-show”隱藏掉多余的列表元素。
無(wú)限滾動(dòng)條
如果你希望進(jìn)一步了解該方法,可訪問(wèn) http://binarymuse.github.io/ngInfiniteScroll/
四、七大調(diào)優(yōu)法則
1. 渲染沒(méi)有數(shù)據(jù)綁定的列表
這是最明顯的解決方案,因?yàn)閿?shù)據(jù)綁定是性能問(wèn)題最可能的根源。如果你只想顯示一次列表,并不需要更新、改變數(shù)據(jù),放棄數(shù)據(jù)綁定是絕佳的辦法。不過(guò)可惜的是,你會(huì)失去對(duì)數(shù)據(jù)的控制權(quán),但除了該法,我們別無(wú)選擇。進(jìn)一步了解: https://github.com/Pasvaz/bindonce。
2.不要使用內(nèi)聯(lián)方法計(jì)算數(shù)據(jù)
為了在控制器中直接過(guò)濾列表,不要使用可獲得過(guò)濾鏈接的方法。“ng-repeat”會(huì)評(píng)估每個(gè) [$digest(http://docs.angularjs.org/api/ng.$rootScope.Scope#$digest)%5D表達(dá)式。在我們的案例中,“filteredItems()”返回過(guò)濾鏈接。如果評(píng)估過(guò)程很慢,它將迅速降低整個(gè)應(yīng)用的速度。
- <li ng-repeat="item in filteredItems()">//這并不是一個(gè)好方法,因?yàn)橐l繁地評(píng)估。
- <li ng-repeat="item in items">//這是要采用的方法
3.使用兩個(gè)列表(一個(gè)用來(lái)進(jìn)行視圖顯示,一個(gè)作為數(shù)據(jù)源)
將要顯示的列表與總的數(shù)據(jù)列表分開(kāi),是非常有用的模型。你可以對(duì)一些過(guò)濾進(jìn)行預(yù)處理,并將存于緩存中的鏈接應(yīng)用到視圖上。下面案例展示了基本實(shí)現(xiàn)過(guò)程。filteredLists變量保存著緩存中的鏈接,applyFilter方法來(lái)處理映射。
- /* 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>
#p#
4.在其他模板中使用ng-if來(lái)代替ng-show
如果你用指令、模板來(lái)渲染額外的信息,例如通過(guò)點(diǎn)擊來(lái)顯示列表項(xiàng)的詳細(xì)信息,一定要使用 ng-if(AngularJSv. 1.1.5以后)。ng-if可阻止渲染(與ng-show相比)。所以其它DOM和數(shù)據(jù)綁定可根據(jù)需要進(jìn)行評(píng)估。
- <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等指令
使用內(nèi)部指令,像ng-mouseenter,AngularJS會(huì)使你的頁(yè)面閃爍。瀏覽器的幀速率通常低于每秒30幀。使用jQuery創(chuàng)建動(dòng)畫(huà)、鼠標(biāo)懸浮效果可以解決該問(wèn)題。確保將鼠標(biāo)事件放入jQuery的.live()函數(shù)中。
6.關(guān)于過(guò)濾的小提示:通過(guò)ng-show隱藏多余的元素
對(duì)于長(zhǎng)列表,使用過(guò)濾同樣會(huì)減低工作效率,因?yàn)槊總€(gè)過(guò)濾都會(huì)創(chuàng)建一個(gè)原始列表的子鏈接。在很多情況下,數(shù)據(jù)沒(méi)有變化,過(guò)濾結(jié)果也會(huì)保持不變。所以對(duì)數(shù)據(jù)列表進(jìn)行預(yù)過(guò)濾,并根據(jù)情況將它應(yīng)用到視圖中,會(huì)大大節(jié)約處理時(shí)間。
在ng-repeat指令中使用過(guò)濾器,每個(gè)過(guò)濾器會(huì)返回一個(gè)原始鏈接的子集。AngularJS 從DOM中移除多余元素(通過(guò)調(diào)用 $destroy),同時(shí)也會(huì)從$scope中移除他們。當(dāng)過(guò)濾器的輸入發(fā)生改變時(shí),子集也會(huì)隨著變化,元素必須進(jìn)行重新鏈接,或著再調(diào) 用$destroy。
大部分情況下,這樣做很好,但一旦用戶經(jīng)常過(guò)濾,或者列表非常巨大,不斷的鏈接與銷毀將影響性能。為了加快過(guò)濾的速度,你可以使用ng-show 和ng-hide指令。在控制器中,進(jìn)行過(guò)濾,并為每項(xiàng)添加一個(gè)屬性。依靠該屬性來(lái)觸發(fā)ng-show。結(jié)果是,只為這些元素增加ng-hide類,來(lái)代 替將它們移除子列表、$scope和DOM。
觸發(fā)ng-show的方法之一是使用表達(dá)式語(yǔ)法。ng-show的值由表達(dá)式語(yǔ)法來(lái)確定??梢钥聪旅娴睦樱?/p>
- <input ng-model="query"></input>
- <li ng-repeat="item in items" ng-show="([item.name] | filter:query).length">{{item.name}}
- </li><span style="font-size: 14px; line-height: 24px; font-family: Helvetica, Tahoma, Arial, sans-serif; white-space: normal;"></span>
另一個(gè)方法是為ng-show傳遞一個(gè)屬性,并在單獨(dú)的子控制器進(jìn)行計(jì)算。該方法稍有點(diǎn)復(fù)雜,但卻是更明晰的方法。
7.關(guān)于過(guò)濾的小提示:防抖動(dòng)輸入
解決第6點(diǎn)提出的持續(xù)過(guò)濾問(wèn)題的另一個(gè)方法是防抖動(dòng)用戶輸入。例如,如果用戶輸入一個(gè)搜索關(guān)鍵詞,只當(dāng)用戶停止輸入后,過(guò)濾器才會(huì)被激活。使用該防抖動(dòng)服務(wù)的一個(gè)很好的解決方案請(qǐng)見(jiàn):http://jsfiddle.net/Warspawn/6K7Kd/。將它應(yīng)用到你的視圖及控制器中,如下所示。
- /* 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>
原文鏈接:http://tech.small-improvements.com/2013/09/10/angularjs-performance-with-large-lists/