1. 程式人生 > >AngularJS渲染性能分析

AngularJS渲染性能分析

log 結束 opera 初始化 roots test com 可能 簡單

作者:Jiang, Jilin


AngularJS中,通過數據綁定。能夠十分方便的構建頁面。可是當面對復雜的循環嵌套結構時,渲染會遇到性能瓶頸。今天,我們將通過一些列實驗,來測試AngularJS的渲染性能,對照ng-show。ng-if的使用場景。並對優化進行簡要分析。

只是在此之前,我們須要先簡單過一遍AngularJS相關的代碼:

$apply: function(expr) {
  try {
    beginPhase(‘$apply‘);
    try {
      return this.$eval(expr);
    } finally {
      clearPhase();
    }
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    try {
      $rootScope.$digest();
    } catch (e) {
      $exceptionHandler(e);
      throw e;
    }
  }
},

beginPhase和clearPhase用於對$rootScope.$$phase進行鎖定。假設發現反復進入$apply階段則拋出異常。以免出現死循環。

$eval: function(expr, locals) {
  return $parse(expr)(this, locals);
},


$parse調用的是$ParseProvider。

因為之後的實驗expr不傳值。所以$ParseProvider會直接返回空函數noop() {}。

因此我們就不做詳細的$ParseProvider內容分析了。



在運行完$eval後。會調用$digest方法。

讓我們看看$digest裏有些什麽:

$digest: function() {
  var watch, value, last,
      watchers,
      length,
      dirty, ttl = TTL,
      next, current, target = this,
      watchLog = [],
      logIdx, logMsg, asyncTask;

  beginPhase(‘$digest‘);
  // Check for changes to browser url that happened in sync before the call to $digest
  $browser.$$checkUrlChange();

  if (this === $rootScope && applyAsyncId !== null) {
    // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
    // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
    $browser.defer.cancel(applyAsyncId);
    flushApplyAsync();
  }

  lastDirtyWatch = null;

  do { // "while dirty" loop
    dirty = false;
    current = target;

    while (asyncQueue.length) {
      try {
        asyncTask = asyncQueue.shift();
        asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
      } catch (e) {
        $exceptionHandler(e);
      }
      lastDirtyWatch = null;
    }

    traverseScopesLoop:
    do { // "traverse the scopes" loop
      if ((watchers = current.$$watchers)) {
        // process our watches
        length = watchers.length;
        while (length--) {
          try {
            watch = watchers[length];
            // Most common watches are on primitives, in which case we can short
            // circuit it with === operator, only when === fails do we use .equals
            if (watch) {
              if ((value = watch.get(current)) !== (last = watch.last) &&
                  !(watch.eq
                      ?

equals(value, last) : (typeof value === ‘number‘ && typeof last === ‘number‘ && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; watchLog[logIdx].push({ msg: isFunction(watch.exp) ?

‘fn: ‘ + (watch.exp.name || watch.exp.toString()) : watch.exp, newVal: value, oldVal: last }); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; } } } catch (e) { $exceptionHandler(e); } } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = ((current.$$watchersCount && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // `break traverseScopesLoop;` takes us to here if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr(‘infdig‘, ‘{0} $digest() iterations reached. Aborting!\n‘ + ‘Watchers fired in the last 5 iterations: {1}‘, TTL, watchLog); } } while (dirty || asyncQueue.length); clearPhase(); while (postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } },


相同的,調用beginPhase改變階段。



$browser.$$checkUrlChange()用於檢測url是否變更。這次我們也用不到:

function fireUrlChange() {
  if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
    return;
  }

  lastBrowserUrl = self.url();
  lastHistoryState = cachedState;
  forEach(urlChangeListeners, function(listener) {
    listener(self.url(), cachedState);
  });
}

接著進行$rootScope和applyAsyncId推斷。假設是根Scope而且存在異步apply請求。則調用$eval並把隊列清空。也不是本次須要用到的部分。

進入循環,asyncQueue保存了$evalAsync方法的數據。

用不到。

之後設置了一個斷點,用於跳出內部循環:

traverseScopesLoop:

循環內推斷是否存在$$watchers列表,然後對watch單元進行變更匹配。每一個頁面的數據綁定都會相應到一個watch單元。此處會檢查是否watch是深匹配,假設為真會調用equals方法進行遞歸檢查,假設watch了一個巨大的對象。那麽equals會十分消耗性能。反之,則會檢查是否是NaN,js中NaN != NaN。然而假設原值和現值都是NaN,事實上是沒有變更過的。

if (watch) {
  if ((value = watch.get(current)) !== (last = watch.last) &&
      !(watch.eq
          ? equals(value, last)
          : (typeof value === ‘number‘ && typeof last === ‘number‘
             && isNaN(value) && isNaN(last)))) {

假設循環後已經發現watch單元原值和現值相等,會跳出循環。

再次又一次驗證,目的是為了防止某個watch調用回調函數後。使得之前的watch現值發生變化。

而當中也設置了ttl循環計數。以免出現watch不斷改變產生死循環的問題。



接著,就是著名的crazy凝視了:

// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast

此處會深度優先遍歷,然後反復上面的檢查。直到遍歷結束。

作者非常貼心的標註一下循環結束了:

// `break traverseScopesLoop;` takes us to here


後面的代碼就十分好懂了,clearPhase。然後處理DigestQueue結束循環。

之後檢查ttl數值,假設ttl值超出了10次(預設值),則會拋出過多循環的異常。

實驗

簡單的過了一遍代碼後。我們開始做一下性能測試:(註:因為不同機器配置性能不同,渲染時間僅作橫向對照之用)

如今。如果我們擁有2個用戶組。每組用戶擁有1000個用戶信息。用戶信息例如以下:

[{name: "user1"}, {name:"user2"},...]

我們第一步做最簡單未經過優化的渲染:

<div>
   <div ng-repeat="user in userList">
      <label>Name</label>
      <p>{{user.name}}</p>
   </div>
</div>

切換分組渲染時間平均310ms左右。

track by

然後簡單使用優化track by優化:

ng-repeat="user in userList track by $index"

第一次渲染260ms左右。之後切換耗費11ms左右。

效果不錯。接著,我們比較不同長度的數組切換比較。如果用戶組1長度仍然為1000,用戶組2長度100:(下圖中,狀態1、2代表綁定數組的切換)

狀態1\狀態2

用戶組1

用戶組2

用戶組1

~0.3ms

~111ms

用戶組2

~175ms

~0.1ms

我們能夠看出,元素動態創建/刪除會極大影響渲染性能。

創建相同數量元素比刪除相同數量元素更消耗性能。

ng-show

基於以上實驗。我們能夠非常easy想到。假設我們使用元素池,預先創建足量的元素。接著通過ng-show來動態調整顯示的元素。這樣性能是否會上升呢?

$scope.getTimes = function(n) {
return new Array(n);
};
<div ng-repeat="i in getTimes(1000) track by $index" ng-show="userList[$index]">
   <label>Name</label>
   <p>{{userList[$index].name}}</p>
</div>

狀態1\狀態2

用戶組1

用戶組2

用戶組1

~1.3ms

~42ms

用戶組2

~22ms

~1.0ms



能夠發現。同組切換時間消耗少量添加。

可是相對的,異組切換性能大幅提升了。

這是因為web中,元素操作是十分消耗性能的操作。因而為了性能。我們須要盡可能避免元素的創建/刪除。相同的,因為每次渲染,都會調用new Array和檢查ng-show屬性,從而導致了同組切換的時間添加了。


技術分享


ng-if與ng-show

Angularjs中還有還有一個方法ng-if,它是僅僅有滿足表達式條件才會變更元素。對於用戶組切換,其毫無疑問會創建/刪除元素。只是在此,我還是把數據羅列一下:

<div ng-repeat="i in getTimes(1000) track by $index" ng-if="userList[$index]">
   <label>Name</label>
   <p>{{userList[$index].name}}</p>
</div>

狀態1\狀態2

用戶組1

用戶組2

用戶組1

~11ms

~250ms

用戶組2

~300ms

~5.5ms


能夠看出,使用緩存+ng-if。性能消耗會比原本沒有track by更消耗性能。

技術分享

那麽ng-if的適用場景是什麽?是否全部的ng-if都適合被ng-show取代呢?讓我們接下去繼續看看列子。

組合

首先。我們對照一下有無緩存的初始化1000條數據的時間。

有緩存

無緩存

用戶組1

~276ms

~240ms

用戶組2

~278ms

~36ms

如今,我們如果用戶有一個id屬性。UI中,依據id是除以5的余數來做不同的渲染。規則例如以下:

余數

渲染元素

0

畫一個2*2的table

1

顯示一個長度為5的ul li列表

2

顯示一個checkbox的input

3

顯示一個textarea

4

顯示一個text input

你可能已經看出我的想法了,我們的目的在於測試。假設存在多個不同渲染方式的情況下,是否適合使用ng-show。我們來看一下,(ng-switch近似ng-if,我們一起增加對照)

<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-show="user.id % 5 === 0">
      <table>
         <tbody>
            <tr>
               <th>11</th>
               <th>12</th>
            </tr>
            <tr>
               <th>21</th>
               <th>22</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-show="user.id % 5 === 1">
      <ul>
         <li>1</li>
         <li>2</li>
         <li>3</li>
         <li>4</li>
         <li>5</li>
      </ul>
   </div>

   <div ng-show="user.id % 5 === 2">
      <input type="checkbox" />
   </div>

   <div ng-show="user.id % 5 === 3">
      <textarea></textarea>
   </div>

   <div ng-show="user.id % 5 === 4">
      <input type="text" />
   </div>
</div>
<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-if="user.id % 5 === 0">
      <table>
         <tbody>
            <tr>
               <th>11</th>
               <th>12</th>
            </tr>
            <tr>
               <th>21</th>
               <th>22</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-if="user.id % 5 === 1">
      <ul>
         <li>1</li>
         <li>2</li>
         <li>3</li>
         <li>4</li>
         <li>5</li>
      </ul>
   </div>

   <div ng-if="user.id % 5 === 2">
      <input type="checkbox" />
   </div>

   <div ng-if="user.id % 5 === 3">
      <textarea></textarea>
   </div>

   <div ng-if="user.id % 5 === 4">
      <input type="text" />
   </div>
</div>


ng-show

ng-if

ng-switch

用戶組1

~557ms

~766ms

~858ms



接著,測試切換:

ng-show

ng-if

ng-switch

組1->組2

~260ms

~257ms

~261ms

組2->組1

~430ms

~470ms

~560ms

好像ng-show各項數值都優於ng-if與ng-switch。只是還沒完,我們繼續改動樣例。

為用戶加入下面幾個屬性,相應綁定於之前定義的元素(m,n初始化時偽隨機生成以便於測試對照數值):

屬性

描寫敘述

matrix

一個m*n的數組

list

一個長度為n的列表

desc

string

checked

boolean


<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-show="user.id % 5 === 0">
      <table>
         <tbody>
            <tr ng-repeat="line in user.matrix track by $index">
               <th ng-repeat="val in line track by $index">{{val}}</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-show="user.id % 5 === 1">
      <ul>
         <li ng-repeat="val in user.list track by $index">{{val}}</li>
      </ul>
   </div>

   <div ng-show="user.id % 5 === 2">
      <input type="checkbox" ng-checked="user.checked" />
   </div>

   <div ng-show="user.id % 5 === 3">
      <textarea ng-model="user.desc"></textarea>
   </div>

   <div ng-show="user.id % 5 === 4">
      <input type="text" ng-model="user.desc" />
   </div>
</div>
<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-if="user.id % 5 === 0">
      <table>
         <tbody>
            <tr ng-repeat="line in user.matrix track by $index">
               <th ng-repeat="val in line track by $index">{{val}}</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-if="user.id % 5 === 1">
      <ul>
         <li ng-repeat="val in user.list track by $index">{{val}}</li>
      </ul>
   </div>

   <div ng-if="user.id % 5 === 2">
      <input type="checkbox" ng-checked="user.checked" />
   </div>

   <div ng-if="user.id % 5 === 3">
      <textarea ng-model="user.desc"></textarea>
   </div>

   <div ng-if="user.id % 5 === 4">
      <input type="text" ng-model="user.desc" />
   </div>
</div>

ng-show

ng-if

ng-switch

用戶組1

~4678ms

~1800ms

~1990ms


是不是大吃一驚?原因非常easy,因為ng-show僅僅是隱藏元素。

可是實際的數據綁定仍舊會被運行。

盡管在頁面上看不到,可是元素綁定的數據還是一並更改了:

技術分享


通過以上實驗,我們非常easy分析出。當頁面布局簡單時,能夠通過ng-show+cachelist來實現高速的數據切換。而當元素組件存在大量元素變化的時候,使用ng-if/ng-switch來避免多余的元素綁定。

通過兩者結合的方式,能夠使得程序在初始化和動態變化的時候保持更好的性能。相同的,在事件處理中。ng-if相較於ng-show會更有利於性能,可是假設事件綁定不多,使用ng-show則更佳。






AngularJS渲染性能分析