AngularJS渲染性能分析
作者: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 |
<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渲染性能分析