大型应用下的 AngularJS 性能
Posted 前端大全
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了大型应用下的 AngularJS 性能相关的知识,希望对你有一定的参考价值。
1 介绍
无论你是为一个拥有大量用户的旧应用编写一个Angalar前端,或已有的Angular应用正在迅速扩张,性能都是一个重要方面。理解什么会导致AngularJS应用程序响应变慢,并且知道在开发过程中对此做出一些权衡是非常重要的。本文将讲述一些AngularJS可能导致的常见性能问题,以及给出在未来如何修复和避免他们的建议。
1.1 需求,假设
本文假设对javascript编程语言和AngularJS比较熟悉。当使用特定于版本的特性,他们会被标注。如果你已经花了一些时间在玩Angular,但还没有认真地处理性能问题,那么你最能吸收这篇文章的要义。
2 行业工具
2.1 基准分析
最出色的代码基准测试工具就是 jsPerf。为了增强可读性,我将在后面相关的部分链接到特定的test runs(测试例子)。
2.2 性能分析
Chrome开发工具有一个很棒的Javascript分析器。我强烈推荐阅读本系列文章。
2.3 Angular Batarang
由Angular 核心团队维护的一个专用的Angular 调试器, 在GitHub上可以获取。
3 软件性能
有两个导致软件性能差的根本原因。
第一个是算法的时间复杂度。解决这个问题很大程度上超出了本文的范围,一般可以这样说,时间复杂度是衡量一个程序需要做多少次的比较来实现一个结果。比较数量越多,程序越慢。一个简单的例子是线性查找与二分查找。线性查找对于同一组数据需要进行更多的比较,因此会慢。时间复杂度的详细讨论,请参考 维基百科文章。
第二个原因是空间复杂度。这是一台电脑运行你的解决方案需要多少“空间”或内存的测量。需要的内存越多,解决方案就越慢。本文将讨论的大多数问题,围绕空间复杂度。详细讨论,请参阅这里.。
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垃圾收集器尽早地释放你的内存。这是通常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;
}
上面快于下面
(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
避免使用delete。例如,给定:
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
这将会在数组中创建一个坑,大大降低操作的性能。
5 重要概念
既然我们已经讨论了JavaScript性能,它对理解一些Angular背后的关键概念很重要。
5.1 Scopes(作用域) 和Digest周期
Angular Scopes本质上是JavaScript对象。他们遵循一个预定义的原型继承规则,深入讨论超出了本文的范围。与本文有关系的,如前所述,使用小Scopes比大Scopes更快。
另一个可以得出的结论是,任何时间,一个新的Scopes被创建,就会为垃圾收集器增加更多需要收集的值。
编写普通的且性能特别的Angular JS应用程序时,digest周期特别重要。实际上,每一个scope中都存储了一个$$watchers函数数组。
每次对scope中的值调用$watch函数,或者一个值被插入或绑定到DOM上,如使用ng-repeat,ng-switch,ng-if,或者其他的DOM属性或元素,都会在最里层的scope的$$watchers数组中添加一个函数。
当scope里的任何值发生变化时, $$watchers中的所有watcher将被触发,如果其中任何一个修改了一个被检测的值, 他们将再次被触发。这将继续下去,直到$$watchers数组不在有任何变化,或AngularJS抛出一个异常。
另外,如果非Angular代码通过 $scope.$apply()运行。这将立即启动digest周期。
最后注意的是,$scope.evalAsync()将在一个异步循环中执行,它不会触发一个新的Digest周期, 它将运行在当前或下一个digest周期的末尾。
6 常见问题:用心设计Angular
6.1 大型对象和服务器调用
所以这一切教会了我们什么?首先,我们应该思考我们的数据模型,努力限制对象的复杂性。这对从服务器返回的对象特别重要。
简单地将整个数据库行强制.toJson()是非常诱人。这里必须要强调:请不要这样做。
使用一个自定义的序列化器,返回Angular应用程序必要的属性的子集。
6.2 监视函数
另一个常见的问题是在watcher或绑定中使用函数。不要将任何指令(ng-show ng-repeat,等等)直接绑定函数。不要直接监测函数的结果。该函数将在每个digest周期运行,这极有可能降缓你的程序。
6.3 监视对象
类似的,Angular 能够通过将scope.$watch第三个可选的参数设置为true 来监视整个对象。说句不好听,这是一个非常糟糕的想法。一个更好的解决方案是依靠服务和对象引用,在scope之间传播对象的变化。
7 列表问题
7.1 大型列表
如果可能的话,避免大型列表。ng-repeat会做一些相当沉重的DOM操作(更不用说污染$$watchers),所以无论是通过分页或无限滚动,试着保持任何列表的渲染使用小型数据。
7.2 过滤器
如果可能的话,避免使用过滤器。他们每个digest循环运行两次,一次是当发生任何变化,另一次是收集进一步的改变,实际上,不从内存删除任何子集,而是简单地用css来过滤。
$index没有什么价值,因为它不再对应于实际的数组索引,而是排序后数组的索引。它还会阻止你释放所有列表的scope。
7.3 更新ng-repeat
同样重要的是避免使用ng-repeat时进行全局列表刷新。在内部,ng-repeat将产生一个$$ hashKey属性,用它来作为集合中的识别项。这意味着做一些像scope.listBoundToNgRepeat = serverFetch() 的操作将导致对整个列表进行一个完整的重新计算,导致对每个个体元素的transcludes运行以及watchers 触发。这是一个非常昂贵的做法。
有两种方法可以解决这个问题。一是维护两个集合和在过滤后的集合上使用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方法只有在迭代对象的一个字段能在集合中保证唯一时才能被使用。对于服务器数据,id属性可以作为天生的tracker。如果没有这个条件,不幸的是,只有自定义同步逻辑是唯一的出路。
8. 渲染问题
Angular 应用缓慢的常见来源是在ng-if或ng-switch上不正确使用ng-hide和ng-show 。区别是重要的,重要性不能在性能的上下文中被夸大。
ng-hide和ng-show简单地切换CSS display属性。在实践中这意味着任何显示或隐藏仍将在页面上,尽管看不见。任何scope将存在,所有$$watchers将触发。
ng-if和ng-switch实际上完全删除或添加DOM。使用用ng-if删除的东西不在scope中。性能优势现在应该很明显,但也是有考究的。具体地说,切换show/hide相对便宜,但切换if/switch相对昂贵。不幸的是这就导致了需要根据不同情况判断使用不同的调用。做出这个决定需要回答的问题是:
How frequently will this change? (the more frequent, the worse fit ng-if is).
这经常会如何变化?(越频繁,使用ng-if越糟糕)。
How heavy is the scope? (the heavyer, the better fit ng-if is).
scope有多大?(越大,越是使用ng-if)。
9. Digest周期问题
9.1 绑定
试着减少你的绑定。Angular 1.3中,有一个新的只进行一次的绑定,语法形状为{ {::scopeValue } }。这将从scope中拿取一次,而不会添加一个watcher到watchers数组。
9.2 $digest() and $apply()
scope.$apply是一个强大的工具,它允许你将值从Angular外部引入到你的应用程序。本质上它会Angular的所有事件(ng-click等)上被触发。问题出现在,scope.$apply始于$rootScope,同时贯穿整个scope链,将导致每个scope触发每个watcher。
另一方面scope.$digest只会在指定的scope内触发,只往下传递。性能优势应该相当不证自明的。折中的方案,当然是,任何父scope将不会收到这个更新,直到下一个循环周期。
9.3 $watch()
scope.$watch()现在已经讨论了好几次。一般来说,scope.$watch()表明糟糕的体系结构。大部分情况下,较低开销的服务和引用的一些组合绑定就能达到相同的结果。如果您必须创建一个watcher,永远记住尽可能地早点解除绑定。你可以通过调用由$watch返回的解绑函数来解除一个watcher,。
var unbinder = scope.$watch('scopeValueToBeWatcher', function(newVal, oldVal){});
unbinder(); //this line removes the watch from $$watchers.
如果你不能过早地解除绑定,记得在$on(‘$destroy’)解除绑定
9.4 $on, $broadcast , and $emit
像$watch,这些都是慢的,因为事件(可能)遍历你的整个scope的层次结构。除此之外,它们会像GOTO一样,让您的应用程序很难调试。幸运的是,像$watch,如果有必要他们可以通过返回函数解除绑定(记得在$on('$destroy')解除绑定,同时可以通过正确地使用服务和scope继承来完全避免)。
9.5 $destroy
如前所述,你应该总是显式调用$on(‘$destroy’),解绑你所有的watchers和事件监听器,并取消任何$timeout实例,或其他正在进行的异步交互。这不仅是确保安全良好的实践,同时标记你的scope让垃圾收集更迅速。不这样做会让他们在后台运行,浪费CPU和RAM。
尤其重要的是要记住的在$destroy 调用中解绑任何在指令元素上定义的DOM事件监听器。不这样做,将会导致在旧浏览器发生内存泄漏和在现代浏览器发生垃圾收集缓慢。一个非常重要的结论是,你需要记住在你移除DOM前调用scope.$destroy。
9.6 $evalAsync
scope.$evalAsync是一个强大的工具,它让你把要执行的操作在当前digest周期的末尾进行排队,不会导致在scope修改后的另一个digest周期。这需要基于具体案例思考,但预期的效果,evalAsync可以大大提高页面的性能。
10 指令问题
10.1 隔离的Scope和Transclusion
隔离Scope 和Transclusion是Angular最令人兴奋的一些事情。他们允许构建可重用、封装的组件,它们在语法上和概念上优雅,让Angular出彩的一个核心部分。
然而,他们有一个权衡。默认情况下,指令不创建一个scope,而是拥有和他们的父元素相同的范围。通过创建一个新的隔离scope或Transclusion,来创建一个新的对象去跟踪和添加新的watch,因此会减慢我们的应用程序。总是在你使用它前停下来并思考有没有必要。
10.2 编译周期
指令编译功能在附加scope之前运行,这是运行任何DOM操作(例如绑定事件)的最佳的地方。从性能的角度来看,需要重要认识的,是元素和属性传递到编译函数使用了原始html模板,它在任何Angular变化前。在实践中这意味着,DOM操作完成,将运行一次,直接使用。另一个重要的点事prelink和postlink的区别。简而言之,prelinks运行由外而内,而postlinks运行由内而外。因此,prelinks提供轻微的性能提升,因为他们阻止内部指令运行第二次digest周期,当父节点在prelink修改scope。然而,子DOM可能不可用。
11 DOM事件问题
Angular提供了许多预先编译好的DOM事件指令. ng-click,ng-mouseenter,ng-mouseleave等等。每次这些事件触发时都会调用scope.$apply()。一个更有效的方法是直接使用addEventListener绑定,然后必要时使用scope.$digest。
12 总结
12.1 AngularJS:不好的部分
ng-click和其他DOM事件
scope.$watch
scope.$on
指令postLink
ng-repeat
ng-show and ng-hide
12.2 AngularJS:好的(性能)部分
track by
使用::只绑定一次
compile和preLink
$evalAsync
服务,作用域继承,通过引用传递对象
$destroy
解绑watches和事件监听器
ng-if和ng-switch
了解更多,https://www.airpair.com/angularjs/posts/angularjs-performance-large-applications#WLXbzSUvg6aWzlUP.99
原文出处:www.airpair.com
译文出处:伯乐在线 - cucr
/////////////////
2. 点击“阅读原文”,查看更多前端文章。
以上是关于大型应用下的 AngularJS 性能的主要内容,如果未能解决你的问题,请参考以下文章