无论你正在编写一个旧的应用程序还是在一个大型应用中采用angularjs,性能是一个重要的方面。了解是什么原因导致angularjs应用程序慢下来非常重要,要知道,在开发过程中做出权衡是很重要的。本文将介绍一些angularjs比较常见的性能问题,以及优化的建议。
2. 性能测试工具
本文采用jsperf http://jsperf.com/ 性能测试的基准。
3. 软件性能
评价软件性能有两个基本的因素:
首先是算法的时间复杂度。一个简单的例子就是线性搜索和二分检索有着非常显著的性能差距。
第二个软件缓慢的原因被称为空间复杂度。这是一台电脑需要多少“空间”或内存运行你的应用程序。内存需求越多,运行速度就越慢。
4 javascript的性能
有些性能问题不仅仅是angular带来的,而是javascript本来就有的。
4.1 循环
避免在循环内部调用函数,可以移到外部调用。
var sum = 0;for(var x = 0; x < 100; x++){ var keys = object.keys(obj); sum = sum + keys[x];}
上面的方面明显没有下面的快:
var sum = 0;var keys = object.keys(obj);for(var x = 0; x < 100; x++){ sum = sum + keys[x];}
4.2 dom访问
在获取dom元素时要注意
angular.element('div.elementclass')
这种方式是非常昂贵的。其实这在angularjs中并不会引起太大的问题。但是留意一下是有好处的。dom树要小,dom的访问要尽可能的少。
4.3 变量作用范围垃圾回收
把你的变量作用范围限制地越紧密越好,这样垃圾回收器就可以更快地回收空间。注意下面的问题:
function demo(){ var b = {childfunction: function(){ console.log('hi this is the child function') }; b.childfunction(); return b;}
当这个函数终上了,这里就没有到b的引用。b就会被回收了。但是如果有这样一行:
var cfunc = demo();
这个引用就会阻止垃圾回收。要尽量避免这类引用。
4.4 数组和对象
这里有很多点:
比如:
for (var x=0; x
for (var x=0; x<100; x++) { i = obj[x].index;}
比这一种更快一点
var keys = object.keys(obj);for (var x = 0; x =0; i--){ var result = _.find(a, function(r){ return (r && r.trackingkey == scope.arr[i].trackingkey); }); if (!result){ scope.arr.splice(i, 1); } else { a.splice(a.indexof(scope.arr[i]), 1); } }_.map(a, function(newitem){ scope.arr.push(newitem);});
这种
比上面的慢些
8 渲染问题
另一个引起angular应用慢的原因是不正确地使用 ng-hide/ ng-show 或 ng-switch。
ng-hide 和 ng-show 简单地对css display属性进行切换。这意味着表面上看不见的东西其实还存在于域中, 所有的$$watchers还是会被触发。
ng-if 和 ng-switch实际上从dom中完全移除了,相应的域也会被移除。性能差异显而易见。
9. 更新周期问题
9.1 绑定
尽量减少你的绑定。在angular 1.3中这里有一个新的一次绑定语法,{{::scopevalue}}。它只会被域执行一次,并不添加到监视器要监视列表中(watcher array).
9.2 $digest() 和 $apply()
scope.$apply 是一个强大的工具,可以让你向angular引入外部的值。本质上它会触发angular的所有事件(例如ng-click)。问题是scope.$apply会从根域$rootscope开始,遍历所有的域链,触发每一个域。
scope.$digest只会执行指定域及其相关的域。两种性能差异不言自明。折中的方案是,不触发任何域等到下一个更新周期再更新。
9.3 $watch()
scope.$watch() 已经在很多场景被讨论过的。基本上scope.$watch是不好的设计的一个标志。如果你非要创建一个观察者。记住对它尽可能地解绑。你可以用$watch的返回函数解绑。
var unbinder = scope.$watch('scopevaluetobewatcher', function(newval, oldval) { });unbinder(); //这一行将watcher从 $$watchers 中移除。
如果你不能早一点解绑,记住在 $on('$destroy') 中进行解绑。
9.4 $on, $broadcast 和 $emit
像$watch一样,他们都是一些很慢的事件,(有可能)遍历整个作用域。他们可能像goto一样,让你的程序无法调试。不过幸运地是像$watch一样,他们都可以在完全不需要的时侯解绑。比如在 $on('$destroy')中。
9.5 $destroy
像前面提到的那样,你应该在$on('$destroy')中解绑你所有的事件侦听器,取消任何$timeout的实例,或者任何其它异步执行的交互。这不仅仅是确保安全。还可以让你的域更快地被垃圾回收。不这样做,他们会一直在后台运行。直接你清空cpu和ram。
另外,解绑dom上的事件侦听器也非常重要,不这样做很可能在老式浏览器中引起内存泄露。
9.6 $evalasync
scope.$evalasync是一个强大的工具。它可以在当前域中执行,并不触发域的更新。evalasync可以极大地提高你网页的性能。
10 指令问题
10.1 隔离的域(isolate scope)和transclusion
域隔离和transclusion是angular最另人激动的特性,它们是angular的核心组件。
但是这里也有一些权衡,指令不能直接创建一个替换他们父组元素的域。通过隔离的域或transclusion我们可以创建一个新的对象去跟踪,添加新的监视器,但是这也会降低应用的性能。在添加之前应该仔细想一想有没有这个必要。
10.2 编绎周期
指令(directive)的compile函数是在域被附加前操作dom的完美功能(比如说绑定事件)。一个很重要的性能方面是,传入compile函数的元素和属性以原始html模板呈现。只会被运行一次,接下来会直接使用。另外一个重要的点是prelink和postlink的区别。prelink从外向内执行。postlinks从内向外执行。prelink性能稍好一些,因为它不会产生第二次更新周期。但是这时子元素的dom还未被创建。
11 dom事件问题
angular提供了很多预定义的dom事件指令。ng-click,ng-mouseenter,ng-mouseleave等等。当调用scole.$apply()时这些事件都会被执行。另外一种更有效率的方式是直接在dom上面绑定addeventlistener,并且尽量使用scope.$digest
优化实例
测试一个应用框架确实是个严峻的挑战,当用户点击日志中任何一个单词,我们就要搜索出相关信息,而页面上可以点击的元素又不计其数;我们想让日志的分页功能也瞬间得到反馈。我们其实已经预先获取到了下一页面的日志数据,所以用户接口的更新就成为了瓶颈,如果拿 angularjs直接实现日志视图的换页功能需要1.2秒,但是如果仔细优化一下的话就可以降到35毫秒。这些优化被证明在应用的其他部分也是适用的,并且对angularjs适应性也很好。但我们必须打破一些规则来实现我们的想法,稍后讨论。
一个github更新的日志demo
an angularjs log viewer
本质上,日志视图就是一个日志消息的列表,每个字都可以点击。所以把angular的指令加到dom元素中,简单实现如下:
{{token | formattoken}}
在单页面应用中有个数千个tokens是很正常的,在早期的测试中,我们发现进入日志的下一页会花费好几秒来执行javascript。更糟的是,不相关的操作(比如点击导航下拉框)延迟也不轻,angularjs的大神说最好把数据元素绑定的数量控制在200以下。对于一个单词就是一个元素的我们来说,早已远超这个数。
分析:
用chrome的javascript profiler工具,我们可以快速定位两个拖延点。首先,每次更新要花大量时间在dom元素的创建和销毁上,如果新的view有不同的行数,或者任何一行有不同数量单词,angular的ng-repeat指令就会创建或者销毁dom元素,这个代价太大了。
其次,每一个单词都有自己的change watcher,angularjs会watch这些单词,一旦鼠标点击就会触发,这个是影响不相关操作(下拉菜单导航)延迟的罪魁祸首。
优化#1:缓存dom elements
我们创建了一个ng-repeat指令的变体,在我们的版本中,如果绑定数据的数量减少了,超出的dom元素会隐藏而不是销毁,如果元素的数量过会儿有增加了,我们会重用这些缓存的元素。
优化#2:aggregate watchers
用来调用change watchers的所有时间大部分都浪费了,在我们的应用中,特定单词上的数据绑定都是永远不会改变的除非整个日志消息变化,为了达成这一点,我们创建了一个指令”hides“隐藏掉了子元素的change watchers,只有等特定父元素表达式修改的时候才会调用他们。就这样,我们避免了在每一次鼠标点击或者其他微小的修改而导致的全盘change watchers(为了实现这个想法,我们稍微修改了angularjs的抽象层,我们稍后再细说)。
优化#3:推迟元素创建
前面说了,我们为日志里的每一个单词单独创建了dom,我们可以利用每一行的单个dom元素得到相同的视觉呈现;其他元素都是为响应鼠标点操作而创建的,因此,我们决定推迟这部分创建,只有当鼠标移动到某行的时候我们再创建他。
为了实现这个,我们为每一行创建了两个版本,一个就是简单的文本元素来显示完整的日志信息,另外一行就是个占位符,用来显示最终为每一个单词填充后的效果。这个占位符开始是隐藏的,当鼠标移动到那一行的时候才会显示,而简单文本那一行这个时候就隐藏掉。下面会讲到,显示占位符是如何填充单词元素的。
优化#4:避开对隐藏元素的监视
我们创建了另外一个指令,用来阻止对隐藏元素的监视,这个指令支持优化#1,相较于原数据,我们多了更多的隐藏dom节点,所以必须消除对多出来的dom节点的监视。这也支持优化#3,让推迟单词节点的创建更加容易。因为直到这行数据的tokenized版本出现我们才会创建他 。
下面的代码就是所有的优化后的样子,我们自定义的指令是粗体显示。
{{logline | formatline }} {{token | formattoken }}
sly-repeat 是ng-repeat的变体,用来隐藏多出来的dom元素而不是销毁他们,sly-evaluate-only-when阻止内部change watchers除非“loglines”变量修改,sly-prevent-evaluation-when-hidden主要负责当鼠标移动到指定行的上面的时候,隐藏的div才显示。
这里展示出了angularjs对于封装和分离的控制力,我们做了复杂的优化但是并没有影响模板的结构(这里展示的代码并不是真正产品里的代码,但是他展示了所有的要点)。
结果:
我们来看一下效果,我们添加了一些代码来衡量,从鼠标点击开始,一直到angular's $digest循环结束(意味着更新dom结束)。
我们衡量点击”下一页“按钮的性能是通过tomcat日志,环境用的是macbook pro上的chrome,结果见下表(每个数据都是10次测试的平均值):
数据已经缓存 从服务器获取数据
简单实现 1190 ms 1300 ms
优化后 35 ms 201 ms
这些数据不包括浏览器用在dom布局和重绘(javascript执行完成后)的时间,每次大概30毫秒。尽管如此,效果也显而易见;下一页的响应时间从1200毫秒骤降至35毫秒(如果算上渲染是65毫秒)。“从服务器获取数据”里的数据包括了我们使用ajax从后端获取log数据的时间。这个跟点击下一页按钮不同,因为我们预取下一页的log数据,但是或许适用于其他的ui响应。即使这样,优化后的程序也可以做到实时更新。