Angular 的性能优化
目录
- 序言
- 变更检查机制
- 性能优化原理
- 性能优化方案
- 小结
- 参考
序言
本文将谈一谈 Angular 的性能优化,并且主要介绍与运行时相关的优化。在谈如何优化之前,首先我们需要明确什么样的页面是存在性能问题?好的性能的衡量指标是什么?性能优化背后的原理又是如何的?如果你对这些问题感兴趣,那么就请继续读下去。
变更检测机制
不同于网络传输优化,运行时优化更加关注于 Angular 的运行机制以及如何编码才能有效地避免性能问题(最佳实践)。而要弄明白 Angular 的运行机制,首先需要理解它的变更检测机制(也被称为脏检查)——如何将状态的变更重新渲染到视图之中。而如何将组件状态的变化反应到视图中,也是前端三大框架都需要解决的一个问题。不同框架的解决方案既有类似的思路也有各自的特色。
首先,Vue 和 React 都是采用虚拟 DOM 来实现视图更新,不过具体实现上还是有所区别:
对于 React:
- 通过使用
setState或forceUpdate来触发render方法更新视图 - 父组件更新视图时,也会判断是否需要
re-render子组件
对于 Vue:
- Vue 会遍历
data对象的所有属性,并使用Object.defineProperty把这些属性全部转为经过包装的getter和setter - 每个组件实例都有相应的
watcher实例对象,它会在组件渲染的过程中把属性记录为依赖 - 当依赖项的
setter被调用时,会通知watcher重新计算,从而使它关联的组件得以更新
而 Angular 则是通过引入 Zone.js 对异步操作的 API 打补丁,监听其触发来进行变更检测。关于 Zone.js 的原理在之前的一篇文章中有详细的介绍。简单来说,Zone.js 通过 Monkey patch (猴补丁)的方式,暴力地将浏览器或 Node 中的所有异步 API 进行了封装替换。
比如浏览器中的 setTimeout:
let originalSetTimeout = window.setTimeout;
window.setTimeout = function(callback, delay) {
return originalSetTimeout(Zone.current.wrap(callback), delay);
}
Zone.prototype.wrap = function(callback) {
// 获取当前的 Zone
let capturedZone = this;
return function() {
return capturedZone.runGuarded(callback, this, arguments);
};
};
或者 Promise.then方法:
let originalPromiseThen = Promise.prototype.then;
// NOTE: 这里做了简化,实际上 then 可以接受更多参数
Promise.prototype.then = function(callback) {
// 获取当前的 Zone
let capturedZone = Zone.current;
function wrappedCallback() {
return capturedZone.run(callback, this, arguments);
};
// 触发原来的回调在 capturedZone 中
return originalPromiseThen.call(this, [wrappedCallback]);
};
Zone.js 在加载时,对所有异步接口进行了封装。因此所有在 Zone.js 中执行的异步方法都会被当做为一个 Task 被其统一监管,并且提供了相应的钩子函数(hooks),用来在异步任务执行前后或某个阶段做一些额外的操作。因此通过 Zone.js 可以很方便地实现记录日志、监控性能、控制异步回调执行的时机等功能。
而这些钩子函数(hooks),可以通过Zone.fork()方法来进行设置,具体可以参考如下配置:
Zone.current.fork(zoneSpec) // zoneSpec 的类型是 ZoneSpec
// 只有 name 是必选项,其他可选
interface ZoneSpec {
name: string; // zone 的名称,一般用于调试 Zones 时使用
properties?: { [key: string]: any; } ; // zone 可以附加的一些数据,通过 Zone.get('key') 可以获取
onFork: Function; // 当 zone 被 forked,触发该函数
onIntercept?: Function; // 对所有回调进行拦截
onInvoke?: Function; // 当回调被调用时,触发该函数
onHandleError?: Function; // 对异常进行统一处理
onScheduleTask?: Function; // 当任务进行调度时,触发该函数
onInvokeTask?: Function; // 当触发任务执行时,触发该函数
onCancelTask?: Function; // 当任务被取消时,触发该函数
onHasTask?: Function; // 通知任务队列的状态改变
}
举一个onInvoke的简单列子:
let logZone = Zone.current.fork({
name: 'logZone',
onInvoke: function(parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) {
console.log(targetZone.name, 'enter');
parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source)
console.log(targetZone.name, 'leave'); }
});
logZone.run(function myApp() {
console.log(Zone.current.name, 'queue promise');
Promise.resolve('OK').then((value) => {console.log(Zone.current.name, 'Promise', value)
});
});
最终执行结果:

理解了 Zone.js 的原理之后,通过走读 Angular 的源码,可以知道 Zone.js 在 Angular 被用来实现只要有异步方法或事件的调用,就会触发变更检测。大体如下:
首先,在 applicatoin_ref.ts 文件中,当 ApplicationRef 构建时就订阅了微任务队列为空的回调事件,其调用了 tick 方法(即变更检测):

其次,在 checkStable 方法中,会判断当微任务队列清空时触发 onMicrotaskEmpty 事件(结合上来看,等价于会触发变更检测):

最后,能够触发 checkStable 方法的调用的地方分别在 Zone.js 的三个钩子函数中,分别是 onInvoke 、 onInvokeTask 和 onHasTask:

比如 onHasTask —— 检测到有或无 ZoneTask 时触发的钩子:

另外 Zone.js 中对于异步任务总共分为三类:
Micro Task(微任务):由 Promise 等创建, native 的 Promise 是在当前事件循环结束前就要执行的,而打过补丁的 Promise 也会在事件循环结束前执行。
Macro Task (宏任务):由 setTimeout 等创建,native 的 setTimeout 会在将来某个时间被处理。
Event Task :由 addEventListener 等创建,这些 task 可能被触发多次,也可能一直不会被触发。
其实如果站在浏览器的角度, Event Task 其实可以看做是宏任务,换句话说,所有事件或异步 API 都可以理解成是宏任务或微任务中的一种,而它们的执行顺序在之前的一篇文章中有详细分析,简单来说:
(1)主线程执行完后,会优先检查微任务队列是否还有任务需要执行
(2)第一次轮询结束后,会检查宏任务队列是否还有任务执行,执行完之后检查微任务列表是否还有任务执行,之后将重复这个过程
性能优化原理
页面性能的好坏,最直观的判断是看页面响应是否流畅、是否响应得快。而页面响应其本质上就是把页面状态的变更重新渲染到页面上的过程,站在相对宏观的视角来看, Angular 的变更检测其实只是整个事件响应周期中的一环。用户与页面的所有交互都是通过事件来触发,其整个响应过程大致如下:

如果考虑优化页面响应的速度,可以从各个阶段入手:
(1)对于触发事件阶段,可以减少事件的触发,来减少整体的变更检测次数和重新渲染
(2)对于 Event Handler 执行逻辑阶段,可以通过优化复杂代码逻辑来减少执行时间
(3)对于 Change Detection 检测数据绑定并更新 DOM 阶段,可以减少变更检测和模板数据的计算次数来减少渲染时间
(4)对于浏览器渲染阶段,则可能需要考虑使用不同浏览器或从硬件配置上进行提升
对于第二、四阶段的相关优化这里不做过多讨论,结合上面提到的 Angular 对于异步任务的分类,针对第一、三阶段的优化方式可以进一步明确:
(1)针对 Macro task 合并请求,尽量减少 tick 的次数
(2)针对 Micro task 合并 tick
(3)针对 Event task 减少 event 的触发和注册事件
(4)tick 分为 check 和 render 两个阶段,减少 check 阶段的计算以及不必要的渲染
前面有提到,大多数情况通过观察页面是否流畅可以判断页面的是否存在性能问题。虽然这种方式简单、直观,但也相对主观,并非是通过精确的数字反映页面的性能到底如何。换言之,我们需要用一个更加有效、精确的指标来衡量什么样的页面才是具备良好性能的。而 Angular 官方也提供了相应的方案,可以通过开启 Angular 的调试工具,来实现对变更检测循环(完成的 tick)的时长监控。
首先,需要使用 Angular 提供的 enableDebugTools 方法,如下:

之后只需要在浏览器的控制台中输入 ng.profiler.timeChangeDetection() ,即可看到当前页面的平均变更检测时间:

从上面可以看出,执行了 692 次变更检测循环(完整的事件响应周期)的平均时间为 0.72 毫秒。如果多运行几次,你会发现每次运行的总次数是不一样、随机的。
官方提供了这样一个判断标准:理想情况下,分析器打印出的时长(单次变更检测循环的时间)应该远低于单个动画帧的时间(16 毫秒)。一般这个时长保持在 3 毫秒下,则说明当前页面的变更检测循环的性能是比较好的。如果超过了这个时长,则就可以结合 Angular 的变更检测机制分析一下是否存在重复的模板计算和变更检测。
性能优化方案
在理解 Angular 优化原理的基础上,我们就可以更有针对性地去进行相应的性能优化:
(1)针对异步任务 ——减少变更检测的次数
- 使用 NgZone 的 runOutsideAngular 方法执行异步接口
- 手动触发 Angular 的变更检测
(2)针对 Event Task —— 减少变更检测的次数
- 将 input 之类的事件换成触发频率更低的事件
- 对 input valueChanges 事件做的防抖动处理,并不能减少变更检测的次数

如上图,防抖动处理只是保证了代码逻辑不会重复运行,但是 valueChanges 的事件却随着 value 的改变而触发(改变几次,就触发几次),而只要有事件触发就会相应触发变更检测。
(3)使用 Pipe ——减少变更检测中的计算次数
将 pipe 定义为 pure pipe(
@Pipe默认是 pure pipe,因此也可以不用显示地设置pure: true)import { Piep, PipeTransform } from '@angular/core'; @Pipe({
name: 'gender',
pure,
})
export class GenderPiep implements PipeTransform {
transform(value: string): string {
if (value === 'M') return '男';
if (value === 'W') return '女';
return '';
}
}
关于 Pure/ImPure Pipe:
Pure Pipe:如果传入 Pipe 的参数没有改变,则会直接返回之前一次的计算结果
ImPure Pipe:每一次变更检测都会重新运行 Pipe 内部的逻辑并返回结果。(简单来说, ImPure Pipe 就等价于普通的 formattedFunction,如果一个页面触发了多次的变更检测,那么 ImPure Pipe 的逻辑就会执行多次)
(4)针对组件 ——减少不必要的变更检测
- 组件使用 onPush 模式
- 只有输入属性发生变化时,该组件才会检测
- 只有该组件或者其子组件中的 DOM 事件触发时,才会触发检测
- 非 DOM 事件的其他异步事件,只能手动触发检测
- 声明了 onPush 的子组件,如果输入属性未变化,就不会去做计算和更新
@Component({
...
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class XXXComponent {
....
}
在 Angular 中 显示的设置 @Component 的 changeDetection 为 ChangeDetectionStrategy.OnPush 即开启 onPush 模式(默认不开启),用 OnPush 可以跳过某个组件或者某个父组件以及它下面所有子组件的变化检测,如下所示:

(5)针对模板 ——减少不必要的计算和渲染
- 列表的循环渲染使用 trackBy
- 尽量使用缓存值,避免使用方法调用和 get 属性的调用
- 模板中如果确实有需要调用函数的地方,且是多处调用可以使用模板缓存
- ngIf 控制组件的展示,放到调用组件的地方控制
(6)其他编码优化建议
- 不要使用 try/catch 来做流程控制,其会造成很大的时间消耗(记录大量堆栈信息等)
- 过多的动画会导致页面加载卡顿
- 长列表可以使用虚拟滚动
- 针对 preload module 尽量延迟 load, 因为浏览器的 http 请求线程的并发数是有限制的,一旦超过了限制数,后面的请求都会被阻塞挂起
- 等等
小结
(1)简要讲解了 Angular 是如何使用 Zone.js 来实现变更检测的
(2)在理解了 Angular 的变更检测的基础上,进一步明确了 Angular 性能优化的原理以及判断页面是否具备良好的性能的标准
(3)针对性的提供了一些偏运行时的性能优化方案
参考
Angular 的性能优化的更多相关文章
- Angular移除不必要的$watch之性能优化
双向绑定是Angular的核心概念之一,它给我们带来了思维方式的转变:不再是DOM驱动,而是以Model为核心,在View中写上声明式标签.然后,Angular就会在后台默默的同步View的变化到Mo ...
- [转]Angular移除不必要的$watch之性能优化
双向绑定是Angular的核心概念之一,它给我们带来了思维方式的转变:不再是DOM驱动,而是以Model为核心,在View中写上声明式标签.然后,Angular就会在后台默默的同步View的变化到Mo ...
- Angular性能优化实践——巧用第三方组件和懒加载技术
应该有很多人都抱怨过 Angular 应用的性能问题.其实,在搭建Angular项目时,通过使用打包.懒加载.变化检测策略和缓存技术,再辅助第三方组件,便可有效提升项目性能. 为了帮助开发者深入理解和 ...
- Angular 1 深度解析:脏数据检查与 angular 性能优化
TL;DR 脏检查是一种模型到视图的数据映射机制,由 $apply 或 $digest 触发. 脏检查的范围是整个页面,不受区域或组件划分影响 使用尽量简单的绑定表达式提升脏检查执行速度 尽量减少页面 ...
- Web性能优化-合并js与css,减少请求
Web性能优化已经是老生常谈的话题了, 不过笔者也一直没放在心上,主要的原因还是项目的用户量以及页面中的js,css文件就那几个,感觉没什么优化的.人总要进步的嘛,最近在被angularjs吸引着,也 ...
- 基于AngularJS/Ionic框架开发的性能优化
AngularJS作为强大的前端MVVM框架,虽然已经做了很多的性能优化,但是我们开发过程中的不当使用还是会对性能产生巨大影响. 下面提出几点优化的方法: 1. 使用单次绑定符号{{::value}} ...
- JavaScript 性能优化技巧分享
JavaScript 作为当前最为常见的直译式脚本语言,已经广泛应用于 Web 应用开发中.为了提高Web应用的性能,从 JavaScript 的性能优化方向入手,会是一个很好的选择. 本文从加载.上 ...
- vue中关于v-for性能优化---track-by属性
vue中关于v-for性能优化---track-by属性 最近看了一些react,angular,Vue三者的对比文章,对比来说Vue比较突出的是轻量级与易上手. 对比Vue与angular,Vue有 ...
- 巧用css的border属性完成对图片编辑功能的性能优化
一.需求场景: 最近闲来无事,boss提出了一个要求,研究相关代码并完成一个关于编辑图片功能的性能优化,该功能的主要界面展示如下: 通过了几分钟的短暂试用,发现就是一个简单的裁剪并保存用户选择并上传的 ...
随机推荐
- spring的异常处理
出自于:https://blog.csdn.net/he90227/article/details/46309297 ---- 利用Spring进行统一异常处理的两种方式. 原文:https:// ...
- 【gdal】创建GeoTiff栅格数据
1 //定义转换参数 2 private readonly double[] d_transform = { 69.999999999999972, 0.01, 0.0, 44.99999999999 ...
- (2)hadoop之-----配置免密码登录
ssh-keygen -t rsa 然后一路回车 在家目录下会生成 .ssh 目录 ls -la 查看 进入 .ssh cd .ssh cp ~/.s ...
- linux(2)-----新装linux配置
1.配置本机ip,刚装的Linux无内网ip vi /etc/susconfig/network-scripts/ifcfq-ens33 编辑配置文件 最后一行改为yes service net ...
- CSS样式下border的几种线型
在用border的时候经常会忘记它有多少种线型以及各种线型的写法:每次都得从头开始,或是用到Google.百度之类的,有空整理了一下 (1)none (没有边框,无论边框宽度设为多大) (2)dott ...
- IP掩码的作用
IP地址&IP掩码==网段,即,与上掩码后相同的IP属于同一网段.
- Java 学习:对象和类
对象和类 从认识的角度考虑是先有对象后有类.对象,是具体的事物.类,是抽象的,是对对象的抽象. 从代码运行角度考虑是先有类后又对象.类是对象的模板. 对象:对象是类的一个实例,有状态和行为. 类:类是 ...
- android kotlin 子线程中调用界面UI组件崩溃
UI 只能在主线程内更新,子线程需要更新UI组件时可以这样: fun fuck(){ Executors.newSingleThreadExecutor().execute{ // url reque ...
- 从零开始实现简单 RPC 框架 9:网络通信之心跳与重连机制
一.心跳 什么是心跳 在 TPC 中,客户端和服务端建立连接之后,需要定期发送数据包,来通知对方自己还在线,以确保 TPC 连接的有效性.如果一个连接长时间没有心跳,需要及时断开,否则服务端会维护很多 ...
- [考试总结]noip模拟45
真开心,挂没了.. 考完:"你们怎么第二题打了这么点分,明明一个爆搜就有65pts!!!怎么跟别人打?!" 然后我看了看我的爆搜,30pts. 然后认为自己打爆了... 我又想为什 ...