AngularJS渲染性能分析
Posted claireyuancy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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渲染性能分析的主要内容,如果未能解决你的问题,请参考以下文章