1介绍
无论您是为大型的旧应用程序编写Angular,还是您已有的Angular应用程序在变得庞大,性能是一个重要的方面。 了解AngularJS应用程序减慢的原因以及如何在开发过程中做出权衡很重要。 本文将介绍AngularJS的一些常见的性能问题,以及如何避免和修复的建议。
1.1要求,假设
本文将会假定你对JavaScript语言和AngularJS有些熟悉。 当使用特定版本的功能时,它们将被调用。 为了充分利用这篇文章,最好是用过一段时间Angular,但还没有认真对待性能。
2衡量工具
2.1基准测试
jsPerf是一个很棒的用于对代码进行基准测试的工具。 我将在相关部分结尾给出具体的测试链接,以便阅读。
2.2 分析
Chrome开发工具有一个很好的JavaScript分析器。 我强烈推荐阅读本系列 文章。
2.3 Angular Batarang
Angular Batarang是一个专注于angular的调试器,由Angular Core Team维护,可在GitHub上获得。
3软件性能
决定是否高性能软件有两个根本原因。
第一个是算法时间复杂度。 解决这个问题很大程度上超出了本文的讨论范围,一般来说,时间复杂度是程序需要做多少计算来取得结果的一个衡量标准。 计算的数量越大,程序越慢。 一个简单的例子是线性搜索与二进制搜索。 线性搜索需要对同一组数据进行更多计算,因此将会更慢。 有关时间复杂性的详细讨论,请参阅维基百科文章。
第二个原因就是算法空间复杂度。 这是计算机运行算法需要多少“存储空间”或内存的衡量标准。 需要的内存越多,解决方案越慢。 本文讨论的大多数问题在空间复杂性之下将会变得松动。 详细讨论请看这里。
这句话不知道怎么翻译,有知道的朋友请告知,谢谢。原文:
Most of the problems this article will talk to fall loosely under space complexity. For a detailed discussion, see here.
4 Javascript 性能
这里说下关于JavaScript 性能的几件事情,不一定局限于angular。
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];
}
http://jsperf.com/for-loop-perf-demo-basic
4.2 DOM访问
注意DOM访问,这很重要。
angular.element( 'div.elementClass')
代价昂贵。虽然在AngularJS中出现这种问题的几率很小,但还是有必要了解这一点。这里要说的第二件事就是在可能的情况下,DOM树应该保持较小。
最后,尽可能避免修改DOM和设置内联样式。因为这会导致JavaScript重绘。重绘的深入讨论超出了本文的范围,但这里有一个很棒的参考。
4.3变量作用域和垃圾回收
尽可能严格地将所有变量声明为局部作用域,以使JavaScript垃圾回收器能够更快地释放内存。
这句我是参照上下文推断出来的,翻译的可能有点问题。可看原文:
Scope all variables as tightly as possible to allow the JavaScript garbage collector to free up your memory sooner rather then later.
这是造成JavaScript,特别是Angular缓慢,滞后,不响应非常常见的原因。请注意以下问题:
function demo(){
var b = {childFunction:function(){console.log('hi this is the child function')};
b.childFunction();
return b;
}
当函数终止时,将不再有对b可用的引用,垃圾回收器将释放内存。但是,如果在其他地方有这样的一行:
var cFunc = demo();
我们现在将对象绑定到一个变量并保持对它的引用,从而防止垃圾收集器清理它。虽然这可能是必要的,但重要的是要注意这对对象引用的影响。
4.4数组和对象
这里有很多事需要说下。第一个也是最简单的,数组总是比对象快,数字访问比非数字访问更好。
for(var x = 0; x <arr.length; x ++){
i = arr [x] .index;
}
上面的比下面的代码快
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
此外,请注意,在基于V8的现代浏览器中,具有很少属性的对象表现得明显更快,所以请将属性数量保持在最低限度。
还要注意,JavaScript能让你在数组中混合类型,但这并不是一个好主意:
var oneType = [1,2,3,4,5,6]
var multiType = [“string”,1,2,3,{a:'x'}]
第二次的操作明显比第一个慢得多,不仅仅是因为逻辑更复杂。
http://jsperf.com/array-types-compare-perf
还要避免使用删除。例如,给出:
var arr = [1,2,3,4,5,6];
var arrDelete = [1,2,3,4,5,6];
delete arrDelete [3];
任何arrDelete的迭代都会比arr迭代慢。
http://jsperf.com/delet-is-slow
这将在数组中创建一个undefined值,从而使操作效率更低。
5重要概念
刚才我们已经讨论了JavaScript的性能,这对于理解一些关键的angular概念很重要。
5.1 Scopes 和 Digest 循环
在angular核心,angular Scopes只是简单JavaScript对象。他们遵循预定义的原型继承方案,对此的深入讨论超出了本文的范围。与上述相关的是,小Scopes将比大Scopes更快。
在这一点上可以做出的另一个结论是,任何时间创建新的Scope,垃圾收集器将增加更多的值以便稍后回收。
一般来说,Digest循环对编写Angular JS应用程序和性能尤其重要。实际上,每个Scope都存储$$watchers函数的数组。
每次在一个Scope值上,或者一个绑定在DOM插值,一个ng-repeat,ng-switch,ng-if或者任何其他DOM属性/元素调用$watch,一个函数将被添加到$$watchers数组的最内层Scope。
当scope里面的任何值发生变化时,$$watchers数组中的所有watchers将触发,如果其中任何一个修改了观察值,则它们将会再次全部触发。 这将持续到$$watchers数组的不再改变并完整传递,或者AngularJS抛出异常。
另外,如果不是Angular代码运行$scope.$apply(),这将立即触发digest 循环。
最后要注意的是,$ scope.evalAsync()将在异步循环中运行代码,该循环不会触发另一个digest 循环,并且将在当前/下一个digest 循环结束时运行。
6 常见问题:设计时要注意
6.1大对象和服务器调用。
那么所有这些教导我们什么呢?第一个是考虑我们的数据模型,并限制对象的复杂性。这对于从服务器返回的对象尤其重要。
也就是说,返回整个数据库行,并且强制性的使用.toJson()是非常简单而且诱人的。这不够健壮:请不要这样做。
而是使用自定义序列化程序只返回Angular应用程序必须要用到的keys子集。
6.2观察函数
另一个常见的问题是在观察者或绑定中使用函数。不要将任何东西(ng-show,ng-repeat等)直接绑定到一个函数上。不要直接观察函数结果。这个函数将在每个digest 循环中运行,可能会减慢应用程序的爬网速度。
6.3观察对象
类似地,Angular提供了通过将第三个可选的true参数传递给scope.$watch来观察整个对象的能力。这是一个很糟糕的主意。一个更好的解决方案是依靠服务和对象引用在scopes之间传播对象更改。
7列表问题
7.1长列表
尽可能避免长列表。 ng-repeat会执行一些非常重的DOM操作(更不用说污染的$$watchers),所以尝试并保持渲染数据的列表尽量小,无论是通过分页还是无限滚动。
7.2过滤器
尽可能避免使用过滤器。它们在每个digest 循环运行两次,一次是在有任何更改时,另一次是收集进一步更改,并且实际上没有从内存中删除收集的任何部分,而只是使用CSS屏蔽过滤的项。
这使$ index无效,因为它不再对应于实际的数组索引,而是排序的数组索引。它也阻止您放弃所有列表的scopes。
7.3更新ng-repeat
当使用ng-repeat时,避免全局列表刷新也很重要。ng-repeat将填充一个$$ hashKey属性并标识该集合中的项目。这意味着,做一些像scope.listBoundToNgRepeat = serverFetch()这样的事情将导致整个列表的重新计算,导致运行外部程序并且观察者为每个单独的元素触发。这是一个非常昂贵、耗性能的。
这有两种方法。一个是在过滤集上维护两个集合和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”>
这里可以找到这三种方法的全功能演示。
只需在三个选项之间点击并要求重新获取就可以很好地显示出来。需要注意的是,track by方法仅在这个字段可以保证在循环对象上唯一时才起作用。对于服务器数据,id属性用作自然跟踪器。如果这不可能,不幸的是,自定义同步逻辑是唯一的办法。
8 渲染问题
Angular应用程序慢的常见原因是ng-hide和ng-show 以及 ng-if或ng-switch的不正确使用。这种区别是不容易的,并且对性能的重要性不能夸大。
ng-hide和ng-show只是切换CSS display 属性。这在实践中意味着,任何显示或隐藏的东西仍然在页面上,尽管看不见。任何scopes 将存在,所有的$$watchers都将触发等。
ng-if和ng-switch实际上完全删除或添加DOM。用ng-if删除的东西将没有scope。虽然性能优势应该是显而易见的,但是有一个需要抓住的点。具体来说,切换显示/隐藏比较便宜,但切换if / switch相对较贵。不幸的是,这导致了需要在一个个用例中权衡。作出这个决定需要回答的问题是:
这个变化有多频繁? (越频繁,ng-if 越糟糕)。
scope有多重? (越重,ng-if更适合)。
9。Digest 循环问题
9.1绑定
尝试并尽量减少绑定。从Angular 1.3开始,有一个新的语法,单向绑定 {{::scopeValue}}。这将从scope添加一次,而不向观察者数组添加观察器。
9.2 $digest() 和$apply()
scope.$apply是一个强大的工具,允许您将Angular外的值引入到应用程序中。在所有事件(ng-click等)下,它会触发。问题在于,scope.$apply从$rootScope开始,并遍历整个scope链,导致每个scope都会触发每个观察者。
scope.$digest 则起始于调用它的具体scope,只从那里向下传播。性能优势应该是相当明显的。当然,任何父级scopes 将不会收到此更新,直到下一个digest 循环。
9.3 $watch()
scope.$watch()已经讨论过几次了。一般来说,scope.$watch是一个表现糟糕的架构。当服务和引用绑定的某些组合在较低的开销时也能达到相同的结果,并且开销更少。很少有不能达到相同结果的情况。如果您必须创建一个观察者,请始终记住在第一时间解除绑定。您可以通过调用$watch函数来解除绑定。
var unbinder = scope.$watch('scopeValueToBeWatcher', function(newVal, oldVal){});
unbinder(); //this line removes the watch from $$watchers.
如果你不能尽早解绑,请记得在$on('$destroy')中解绑。
9.4 $on, $broadcast , 和 $emit
像$watch一样,这些都是缓慢的,因为事件(潜在地)必须遍及整个scope 层次结构。除此之外,GOTO还可以让您的应用程序成为一个复杂的调试问题。幸运的是,像$watch一样,他们可以调用返回的函数解绑(请记住在$on('$destroy') 中解除绑定),并且几乎可以避免使用服务和scope 继承。
9.5 $ destroy
如上所述,您应该总是明确地调用 $on('$destroy'),解除所有观察者和事件侦听器的绑定,并取消任何$timeout或其他异步正在进行的交互的实例。这不仅是确保安全的良好做法,更快地标示垃圾收集的scope 。不这样做会让他们在后台运行,浪费你的CPU和RAM。
特别重要的是要记住在$destroy函数调用中取消绑定在directives元素上定义的任何DOM事件侦听器。否则会导致旧版浏览器中的内存泄漏,并在现代浏览器中减慢您的垃圾收集器。一个非常重要的推论是在删除DOM之前调用scope.$destroy。
9.6 $evalAsync
scope.$evalAsync是一个强大的工具,可以让您在当前digest 循环结束时将操作排队执行,而不会使另一个digest 循环的scope 变脏。需要根据具体情况考虑这一点,但是,如果这是预期的效果,evalAsync可以大大提高页面的性能。
10指令问题
10.1独立作用域和嵌入
独立作用域和嵌入是Angular最令人兴奋的事情。它们允许构建可重复使用的封装组件,它们在语法和概念上都很优雅,是Angular的核心部分。
但是,他们也是需要权衡的。默认情况下,指令不会创建一个作用域,而是使用与其父元素相同的作用域。通过使用Isolate Scope或Transclusion创建新的scope,我们会创建一个新对象来跟踪,并添加新的观察者,这减慢我们的应用程序。所以在使用这些技术之前,请先停下来思考。
10.2编译周期
指令的compile函数在scope 附加之前运行,是运行任何DOM操作(例如绑定事件)的理想场所。 从性能的角度来看,重要的是,传递给编译函数的元素和属性表示原始的html模板,在进行任何angular的更改之前。 这意味着在这里完成的DOM操作将运行一次,并始终传播。 经常被忽略的另一个重点是prelink和postlink之间的区别。 简而言之,prelinks 从外而内运行,postlinks 而从内而外运行。 因此,prelinks提供轻微的性能提升,因为当父级修改prelink中的scope时,它们会阻止内部指令运行第二个digest 循环。 但是,子DOM可能是不可用的。
11 DOM事件问题
Angular提供了许多预先定义的DOM事件指令。 ng-click,ng-mouseenter,ng-mouseleave等。 所有这些调用scope.$apply() 每当发生事件时。 一个更有效的方法是直接与addEventListener绑定,然后根据需要使用scope.$digest。
12总结
12.1 AngularJS:糟粕
- ng-click 和 other DOM events
- scope.$watch
- scope.$on
- Directive postLink
- ng-repeat
- ng-show and ng-hide
12.2 AngularJS:精华
- track by
- :: 单次绑定
- compile 和 preLink
- $evalAsync
- Services, scope inheritance, passing objects by reference
- $destroy
- unbinding watches 和 event listeners
- ng-if 和 ng-switch