對AngularJS進行效能調優的7個建議

csdn發表於2013-09-18

  AnglarJS作為一款優秀的Web框架,可大大簡化前端開發的負擔。近日Sebastian Fröstl在一篇博文《AngularJS Performance Tuning for Long Lists》中表示AnglarJS在處理包含複雜資料結構的大型列表時,其執行速度會非常慢。他在文中同時分享瞭解決方案。下面為該文的譯文。

  AnglarJS很棒,但當處理包含複雜資料結構的大型列表時,其執行速度就會非常慢。這是我們將核心管理頁面遷移到AngularJS過程中遇到的問題。這些頁面在顯示500行資料時本應該工作順暢,但首個方法的渲染時間竟花費了7秒,太可怕了。

  後來,我們發現了在實現過程中存在兩個主要效能問題。一個與“ng-repeat ”指令有關,另一個與過濾器有關。

  下文將分享我們通過不同的方法解決效能問題的經驗,希望可以給你帶來啟示。

  AngularJS 中的ng-repeat在處理大型列表時,速度為什麼會變慢?

  AngularJS中的ng-repeat在處理2500個以上的雙向資料繫結時速度會變慢。這是由於AngularJS通過“dirty checking”函式來檢測變化。每次檢測都會花費時間,所以包含複雜資料結構的大型列表將降低你應用的執行速度。

  提高效能的先決條件

  時間記錄指令

  為了測量一個列表渲染所花費的時間,我們寫了一個簡單的程式,通過使用“ng-repeat”的屬性“$last”來記錄時間。時間存放在TimeTracker服務中,這樣時間記錄就與伺服器端的資料載入分開了。

// 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開發者工具的時間軸(Timeline)屬性

  在Chrome開發者工具的時間軸標籤中,你可以看見事件、每秒內瀏覽器幀數和記憶體分配。“memory”工具用來檢測記憶體洩漏,及頁面所需的記憶體。當幀速率每秒低於30幀時就會出現頁面閃爍問題。“frames”工具可幫助瞭解渲染效能,還可顯示出一個JavaScript任務所花費的CPU時間。

  通過限制列表的大小進行基本的調優

  緩解該問題,最好的辦法是限制所顯示列表的大小。可通過分頁、新增無限滾動條來實現。

  分頁

  分頁,我們可以使用AngularJS的“limitTo”過濾器(AngularJS1.1.4版本以後)和“startFrom”過濾器。可以通過限制顯示列表的大小來減少渲染時間。這是減少渲染時間最高效的方法。

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

  如果你不能/不想使用分頁,但過濾過程又很慢,這時一定要檢查前五步,並使用“ng-show”隱藏掉多餘的列表元素。

  無限滾動條

  如果你希望進一步瞭解該方法,可訪問“ http://binarymuse.github.io/ngInfiniteScroll/”,

  七大調優法則

  1.渲染沒有資料繫結的列表

  這是最明顯的解決方案,因為資料繫結是效能問題最可能的根源。如果你只想顯示一次列表,並不需要更新、改變資料,放棄資料繫結是絕佳的辦法。不過可惜的是,你會失去對資料的控制權,但除了該法,我們別無選擇。進一步瞭解: https://github.com/Pasvaz/bindonce

  2.不要使用內聯方法計算資料

  為了在控制器中直接過濾列表,不要使用可獲得過濾連結的方法。“ng-repeat”會評估每個 [$digest( http://docs.angularjs.org/api/ng.$rootScope.Scope#$digest)%5D表示式。在我們的案例中,“filteredItems()”返回過濾連結。如果評估過程很慢,它將迅速降低整個應用的速度。

<li ng-repeat="item in filteredItems()"> //這並不是一個好方法,因為要頻繁地評估。
<li ng-repeat="item in items"> //這是要採用的方法

  3.使用兩個列表(一個用來進行檢視顯示,一個作為資料來源)

  將要顯示的列表與總的資料列表分開,是非常有用的模型。你可以對一些過濾進行預處理,並將存於快取中的連結應用到檢視上。下面案例展示了基本實現過程。filteredLists變數儲存著快取中的連結,applyFilter方法來處理對映。

/* 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

  如果你用指令、模板來渲染額外的資訊,例如通過點選來顯示列表項的詳細資訊,一定要使用  ng-if(AngularJSv. 1.1.5以後)。ng-if可阻止渲染(與ng-show相比)。所以其它DOM和資料繫結可根據需要進行評估。

<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,AngularJS會使你的頁面閃爍。瀏覽器的幀速率通常低於每秒30幀。使用jQuery建立動畫、滑鼠懸浮效果可以解決該問題。確保將滑鼠事件放入jQuery的.live()函式中。

  6.關於過濾的小提示:通過ng-show隱藏多餘的元素

  對於長列表,使用過濾同樣會減低工作效率,因為每個過濾都會建立一個原始列表的子連結。在很多情況下,資料沒有變化,過濾結果也會保持不變。所以對資料列表進行預過濾,並根據情況將它應用到檢視中,會大大節約處理時間。

  在ng-repeat指令中使用過濾器,每個過濾器會返回一個原始連結的子集。AngularJS 從DOM中移除多餘元素(通過呼叫 $destroy),同時也會從$scope中移除他們。當過濾器的輸入發生改變時,子集也會隨著變化,元素必須進行重新連結,或著再呼叫$destroy。

  大部分情況下,這樣做很好,但一旦使用者經常過濾,或者列表非常巨大,不斷的連結與銷燬將影響效能。為了加快過濾的速度,你可以使用ng-show和ng-hide指令。在控制器中,進行過濾,併為每項新增一個屬性。依靠該屬性來觸發ng-show。結果是,只為這些元素增加ng-hide類,來代替將它們移除子列表、$scope和DOM。

  • 觸發ng-show的方法之一是使用表示式語法。ng-show的值由表示式語法來確定。可以看下面的例子:
  • <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>
                
  • 另一個方法是為ng-show傳遞一個屬性,並在單獨的子控制器進行計算。該方法稍有點複雜,但卻是更明晰的方法。

  7.關於過濾的小提示:防抖動輸入

  解決第6點提出的持續過濾問題的另一個方法是防抖動使用者輸入。例如,如果使用者輸入一個搜尋關鍵詞,只當使用者停止輸入後,過濾器才會被啟用。使用該防抖動服務的一個很好的解決方案請見: http://jsfiddle.net/Warspawn/6K7Kd/。將它應用到你的檢視及控制器中,如下所示。

/* 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>

  原文連結: AngularJS Performance Tuning for Long Lists

相關文章