整理AngularJS框架使用过程当中的一些性能优化要点


Posted in Javascript onMarch 05, 2016

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;
}

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">

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适应性也很好。但我们必须打破一些规则来实现我们的想法,稍后讨论。

整理AngularJS框架使用过程当中的一些性能优化要点

一个Github更新的日志demo

An AngularJS log viewer

本质上,日志视图就是一个日志消息的列表,每个字都可以点击。所以把Angular的指令加到DOM元素中,简单实现如下:

<span class='logLine' ng-repeat='line in logLinesToShow'>
 <span class='logToken' ng-repeat='token in line'>{{token | formatToken}} </span>
 
</span>

在单页面应用中有个数千个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版本出现我们才会创建他 。

下面的代码就是所有的优化后的样子,我们自定义的指令是粗体显示。

<span class='logLine' sly-repeat='line in logLinesToShow' sly-evaluate-only-when='logLines'>
 <div ng-mouseenter=”mouseHasEntered = true”>
  <span ng-show='!mouseHasEntered'>{{logLine | formatLine }} </span>
  <div ng-show='mouseHasEntered' sly-prevent-evaluation-when-hidden>
   <span class='logToken' sly-repeat='tokens in line'>{{token | formatToken }}</span>
  </div>
 </div>
 
</span>

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响应。即使这样,优化后的程序也可以做到实时更新。

Javascript 相关文章推荐
jquery简单体验
Jan 10 Javascript
javascript模版引擎-tmpl的bug修复与性能优化分析
Oct 23 Javascript
js获取多个tagname的节点数组
Sep 22 Javascript
jQuery中对未来的元素绑定事件用bind、live or on
Apr 17 Javascript
JavaScript数组对象赋值用法实例
Aug 04 Javascript
js控制div层的叠加简单方法
Oct 15 Javascript
JS限制条件补全问题实例分析
Dec 16 Javascript
Vuex模块化实现待办事项的状态管理
Mar 15 Javascript
vue计算属性get和set用法示例
Feb 08 Javascript
vue实现按需加载组件及异步组件功能
May 27 Javascript
使用Vue实现调用接口加载页面初始数据
Oct 28 Javascript
javascript设计模式 ? 简单工厂模式原理与应用实例分析
Apr 09 Javascript
详解JavaScript的AngularJS框架中的表达式与指令
Mar 05 #Javascript
深入解析AngularJS框架中$scope的作用与生命周期
Mar 05 #Javascript
JS判断字符串字节数并截取长度的方法
Mar 05 #Javascript
jQuery实现滚动鼠标放大缩小图片的方法(附demo源码下载)
Mar 05 #Javascript
js控制TR的显示隐藏
Mar 04 #Javascript
Node.js操作Firebird数据库教程
Mar 04 #Javascript
实例剖析AngularJS框架中数据的双向绑定运用
Mar 04 #Javascript
You might like
最简单的PHP程序--记数器
2006/10/09 PHP
Php获取金书网的书名的实现代码
2010/06/11 PHP
PHP中Fatal error session_start()错误解决步骤
2014/08/05 PHP
PHP利用MySQL保存session的实现思路及示例代码
2014/09/09 PHP
ThinkPHP实现二级循环读取的方法
2014/11/03 PHP
PHP动态页生成静态页的3种常用方法
2014/11/13 PHP
PHP静态文件生成类实例
2014/11/29 PHP
smarty缓存用法分析
2014/12/16 PHP
php对文件进行hash运算的方法
2015/04/03 PHP
如何通过Linux命令行使用和运行PHP脚本
2015/07/29 PHP
php实现简单的上传进度条
2015/11/17 PHP
PHP从尾到头打印链表实例讲解
2018/09/27 PHP
Jquery事件的连接使用示例
2013/06/18 Javascript
jquery选择器排除某个DOM元素的方法(实例演示)
2014/04/25 Javascript
Javascript之Date对象详解
2016/06/07 Javascript
Ajax分页插件Pagination从前台jQuery到后端java总结
2016/07/22 Javascript
基于vuejs+webpack的日期选择插件
2020/05/21 Javascript
详解从0开始搭建微信小程序(前后端)的全过程
2019/04/15 Javascript
前端插件之Bootstrap Dual Listbox使用教程
2019/07/23 Javascript
浅谈bootstrap layer.open中end的使用方法
2019/09/12 Javascript
js前端传json后台接收‘‘被转为quot的问题解决
2020/11/12 Javascript
python中的错误处理
2016/04/10 Python
读写json中文ASCII乱码问题的解决方法
2016/11/05 Python
python和opencv实现抠图
2018/07/18 Python
基于Python数据分析之pandas统计分析
2020/03/03 Python
解决IDEA 的 plugins 搜不到任何的插件问题
2020/05/04 Python
解决pycharm不能自动保存在远程linux中的问题
2021/02/06 Python
pandas统计重复值次数的方法实现
2021/02/20 Python
英国豪华针织品牌John Smedley的在线销售商:The Outlet by John Smedley
2018/04/08 全球购物
领导干部培训感言
2014/01/23 职场文书
2014学雷锋活动总结
2014/03/09 职场文书
交通安全标语
2014/06/06 职场文书
法学专业大学生实习自我鉴定
2014/10/05 职场文书
2015年材料员工作总结
2015/04/30 职场文书
2019年怎样才能撰写出优秀的自荐信
2019/03/25 职场文书
OpenCV图像变换之傅里叶变换的一些应用
2021/07/26 Python