1. 简介
无论你正在编写一个旧的应用程序还是在一个大型应用中采用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<arr.length; x++) {
i = arr[x].index;
}
比这一种快一点(注* arr为数组, obj为json对象)
for (var x=0; x<100; x++) {
i = obj[x].index;
}
比这一种更快一点
var keys = Object.keys(obj);
for (var x = 0; x < keys.length; x++){
i = obj[keys[x]].index;
}
测试 : http://jsperf.com/array-vs-object-perf-demo
5 重要的概念
我们已经讨论过有关JavaScript的性能,现在有必要看一看AngualrJS中的核心概念,看看它究竟是怎么运作的。
5.1 域(Scopes)和更新周期(Digest Cycle)
Angular的域本质上是一些JavaScript对象,它们从一些预定义的对象继承而来。基本上,小的域比大的域运行要快。
换句话说,每创建一个新的域,都会给垃圾回收器添加更多待回收的内容。
在写AngularJS应用中尤其要注意的一个核心概念和性能影响方面是更新周期(Digest Cycle)。实际上每一个域都会存放一个由方法组成的数组 $$watchers。
每当域中的一个值(属性)或绑定的DOM,如 ng-repeat,ng-switch 和 ng-if 等等,调用 $watch 时,一个函数(function)就会添加到相对应域中的$$watchers数组队列中。
当域中的值发生改变时,在$$watchers中所有的watchers函数都会被触发调用。并且当它们的任何一个修改了域中的某个值时,它们会被再次触发执行。
这个过程会一直循环下去直到$$watcher数组队列中不再做任何更改或抛出异常为止。
更外如果任何代码执行$scope.$apply(),都会触发更新周期。
最后一点是 $scope.evalAsync() 会在一个异步调用中执行,并且在当前和下个执行周期中,不会调用其的更新周期。
6. 在设计Angular时应该遵守的一般准则
6.1 大型对象和服务器调用
所以这些都告诉了我们什么?首先我们要尽可能地简化我们的对象。当对象是从服务器返回时,这一点尤为重要。
直接将数据库中的一行转换成对象只是临时性方案,因此不要使用.toJson().
只需要把Angular需要的属性值返回回来。
6.2 监视函数(Watching Functions)
另一个常见的问题是为观察者绑定的函数。不要将任何东西(ng-show, ng-repeat等等)直接绑定到一个函数。不要直接监视任何函数的返回值。该函数会在每个更新周期都执行,可能会降低你应用的速度。
6.3 监视对象(Watching Objects)
同样,Angular提供了第三个可选参数来监视整个对象的改动。将调用$watch的第三个参数设为true。这是一个非常可怕的想法。一个更好的解决办法是依靠服务和对象的引用,监视域之间的变化。
7 列表问题
7.1 长列表(Lists)
尽一些可能避免长列表。ng-repeat会进行了一些很重的DOM操作(更不用说对$$watchers的污染),所以无论是在分页或是在无限滚动中,尽量使用小型数据进行渲染。
7.2 过滤器(Filters)
要尽量避免使用过滤器。他们会在每个更新周期运行两次,每当发生任何改变时运行一次,另一次是收集更深层次的改变时触发。所以不要直接从内部列表中移除对象,使用CSS控制即可。(注* 用添加CSS类名去隐掉他们)
渲染时的 $index 值并不是真正的数组索引值,它豪无价值。但是排好序的数组索引,无法让你遍历到所有列表中的域。
7.3 更新 ng-repeat
当使用ng-repeat时要尽量避免对全局列表的刷新。ng-repeat会产生一个$$hashkey属性和一系统唯一的项。这意味着当你调用 scope.listBoundToNgRepeat = serverFetch() 时会引起对整个列表的重新刷新。会通知执行所有的watchers并触发每一个元素,这是非常消耗性能的。
这里有两种解决方案。一种是维护两个集合,和带有过虑器(filter)的ng-repeat(基本上需要自定义同步逻辑,因此算法更复杂,可维护性更差),另一种方案是使用track by去指定你自己的key(Angular 1.2 开始支持,只需要很少的同步逻辑)。
总之:
scope.arr = mockServerFetch();
会比下面的这种慢
var a = mockServerFetch();
for(var i = scope.arr.length - 1; i >=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);
});
这种
<div ng-repeat="a in arr track by a.trackingKey">
比上面的慢些
<div ng-repeat="a in arr">
测试用例可以在这里 http://plnkr.co/qRlVT52vaMreEkXvhQ3B
找到
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
12 摘要
12.1 AngularJS: 坏的方面
ng-click and other DOM events
scope.$watch
scope.$on
Directive postLink
ng-repeat
ng-show and ng-hide
12.2 AngularJS: 好的方面 (性能)
track by
oneTime bindings with ::
compile and preLink
$evalAsync
Services, scope inheritance, passing objects by reference
$destroy
unbinding watches and event listeners
ng-if and ng-switch
原文地址: airpair.com