什么是性能?前端业务开发关注的性能,主要有两个:加载速度、渲染效率。二者一般也合称【性能体验】。
加载速度
衡量加载速度的传统指标一般是:首字节、DOMContentLoaded、onLoad。传统指标的问题是,完全站在技术视角衡量,并不能代表用户实际的体验。目前受业界广泛认可的是谷歌提出的一套面向用户的指标 Progressive Web Metrics :
- FP(First Paint)白屏结束,有可见像素出现了
- FCP(First Contentful Paint)对用户有意义的内容出现了
- FMP(First Meaningful Paint)同上
- TTI(Time to Interactive)js 执行完了,可以交互了
FP 定义了【白屏时间】,后面两个定义【首屏时间】。从技术角度看怎么知道什么是 Contentful,什么是 Meaningful?基本策略是检测 DOM 节点变化最大的时刻,不过最精确的还是得靠自定义埋点,毕竟这个指标归根结底是用于帮助开发掌握页面在线上运行的实际状况,也只有开发最清楚页面内容的加载过程。TTI 最好也用自定义埋点,靠技术手段检测会 比较麻烦 。
在实践中,我们需要在 关键路径 上的各个节点添加监控埋点,从 url 发出请求,到 DNS 解析,到服务端处理(如果是服务端渲染的话),到 html 开始加载,到阻塞类资源加载,到业务代码开始执行,到接口返回,到内容正式渲染。这样才有利于分析究竟是哪个环节影响了最终性能。
张克军的这张图描绘了网页通用的加载模型,在实际业务中通常还存在其他一些环节。比如移动端 hybrid 页面或 RN 页面的容器初始化过程,比如前置校验用户登录态、获取用户定位等等。
上述指标只是衡量了单个页面的性能,看不到产品整体的性能,因此大厂基本都在搞一个叫【秒开率】的指标。秒开率是指整个样本集中,某指标小于 1s 的占比。秒开率回答了这样一个问题:整个产品有多少页面的性能指标(一般取 90 分位线)小于 1s。
渲染效率
渲染效率就是指页面流畅度,看交互动效是否有卡顿(掉帧)。衡量渲染效率的指标主要是 FPS。查看 FPS 最简单的办法有两个,一是用 stats.js ,二是在 chrome devtools 中通过 command + shift + p 调出命令窗口,输入 fps 即可调出帧率面板。
由于 UI 线程和 js 线程共用同一个线程,js 任务的运行可能导致 UI 卡顿、卡死,因此浏览器还提供了一个用于监控 long task 的 api。
参考: 百度APP流畅度全流程质量监控实践(一) 流畅度现状分析 。
监控
通常我们发现某个页面性能已经很差了,于是一顿专项优化,最后各项指标都达到了预设目标,但问题是怎么保证性能长期不会继续劣化呢?为了知道页面在线上运行的实际性能状况,首先就得把监控体系建立起来,能对采集到的指标数据做各种聚合展示以便随时看到页面指标数据变化,能定期自动生成报表,能对指标异常变化(比如突增、骤降)添加报警。
为了采集性能数据,浏览器提供了一系列专门用于监控性能的 API,例如 navigation-timing、 resource-timing 、 user-timing 等。通用指标可封装 sdk 进行采集上报,sdk 应该同时提供上报自定义测速点的能力。
除了线上监控,性能保障还有赖于事前防控。比如在上线流程,应该有一道性能检测环节(checklist),防止一些明显的、常见的、比较极端的性能劣化场景,比如上了一个特别大的资源而不自知(不小心全量引入了 lodash、加了张没压缩的大图、引入了无用的资源等)。最简单的思路,就是上传到 ligthouse,如果检测得分低于阈值,就直接禁止上线。这种检测既可以在上线前执行,也可以定期循环执行。
性能分析
通常监控已经能提供比较详细的性能状况数据,不过排查问题时通常还需要一些工具辅助以获得更加细节的信息。
浏览器提供了一系列衡量性能的工具,可查看 资源加载时序 、 分析网页性能 、 分析代码执行耗时 、 分析渲染流畅度 等。
现代框架一般也提供了运行时的性能分析工具。比如 react profiler ,可方便的看到组件级乃至方法级的执行耗时(针对【渲染效率】)。
其他分析工具
加载性能优化
性能优化的基本思路,是搞清楚整个加载链路出问题的环节,然后针对性的修复。具体的手段,主要有四种类型:
- 体量规划
- 时序规划
- 距离规划
- 定向优化
其实还有一种严格说只能算体验优化的手段,就是从交互上让用户感觉得快,比如骨架屏,还有图片的渐进式加载(可参考 how medium does progressive image loading )。
体量规划
通常来说,加载资源的总量越小,加载性能越好。针对 web 而言,主要是限制请求资源的体积。单从资源体积的角度看,理想情况是完全的按需加载,即每时每刻仅加载当前需要的资源。
- 只请求当前环境需要的资源
- 增量加载(懒加载、渐进式加载、异步加载、分屏加载...)
- 按需加载 polyfill
- modern 构建模式
- 减小资源体积(雅虎14条之4、10)
- uglify
- Gzip(服务端开启,只压缩文本文件,不要压缩图片这类的二进制文件, 原因 )
- 图片优化(压缩、像移动端一样针对屏幕尺寸和分辨率加载不同大小/质量的图片、根据网络状况加载不同质量的图片、用 webp 格式)
- 控制 cookie 大小
- 控制 header 大小
- 用字符数最少的代码:例如用 void 0 代替 undefined
- 减小无效资源(雅虎14条之4、12)
- 删除无效的代码(Dead Code Elimination)
- 删除重复代码( js-copypaste-detect )
- 移动端字体图标只需要用 ttf
- 避免空 src
时序规划
小学奥数里有个主题叫统筹规划,其中有一类问题就是看如何安排各种事情的执行流,以最小化总时间。时序规划也是类似,最基本的两种思路,就是并发和前置,要么一起搞,要么提前搞。
- 并行
- Promise.all
- facebook BigPipe
- 前置
- SSR:将模板渲染前置到服务端处理
- prefetch、prerender、preload
- 容器预初始化
- 管道:无需等所有数据加载完才处理,而是加载一部分就处理一部分(html 的渲染就是这样的)
距离规划
距离规划的基本原理是:传输速度有上限,因此距离越近,时间越短。距离最近的是寄存器/内存,最远的是服务器。
- CDN (雅虎14条之2)
- 缓存
- http 缓存(雅虎14条之3、13)
- 使用可缓存的 get 请求(雅虎14条之14)
- 服务器缓存
- localStorage
- web worker
- kv-storage
- stale-while-revalidate
- 避免重定向(雅虎14条之11)
- 服务端推送(单向传输,避免一来一回)
通常能够被缓存并且缓存能起到较大作用的,是不常变化、会被反复用到的静态信息。经常变化的信息,或者很少重复使用的信息,会影响缓存的命中率。要利用缓存,在设计上就需要考虑 动静分离 ,比如与用户状态无关的配置信息和与用户状态相关的动态信息分开使用不同接口。
为了让经常变化的数据也能使用缓存来提高效率,也有一种思路:每次都先从缓存里取数据,然后每次都发送请求更新缓存,以时间换空间,以 1 次延迟的代价,来提高接口请求的性能。这种策略还有个专门的规范叫 stale-while-revalidate ,基于 react hook 实现的网络请求方法库 swr 已经内置了该策略。
定向优化
前三种思路属于通用思路,比如 CPU 的性能优化也会采用这些思路。而定向优化是指针对环境的特征,提供专门的优化方案。就 Web 而言,特定环境主要是指:浏览器的请求、加载和渲染机制;http 协议;js 引擎。
针对浏览器机制
- 域名分片(Domain Sharding、SPDY)
- 因为浏览器对单个域名的并发加载数有上限,一般是 6 个
- PC 端可分片 2-4 个为宜
- 移动端 DNS 解析很慢,分片不要超过 2 个
- 针对加载过程的优化
针对 http 1.1
- 减少请求次数(雅虎14条之1)
- 合并资源(bundle / spites)或合并资源的请求(CDN Combo)
- 合并多个 ajax 请求
- CSS inline
- 使用 CSS、SVG、Inline Image、Icon-font 代替图片
- 避免使用 @import 和 iframes
- 控制域名数量,减少 DNS 查询(雅虎14条之9)
- 选择更先进的协议:UDP、QUIC、SPDY、http 2
- 《Web 性能权威指南》
针对 js 引擎
- 使用更高效的 api
- jsperf.com/
- 编写有利于引擎优化的代码。比如按照 asm.js 规范编写的代码,将直接获得引擎层面的优化支持
- 《High Performance Javascript》
优化手段有很多,但收益并不相同,像针对 js 引擎的优化,基本只有框架会去做。有明显收益的,主要是缓存、按需加载、减小资源体积、请求并发/前置,可参考淘宝天猫首页性能对赌优化回忆录(链接: pan.baidu.com/s/1mgILtDfD… 提取码: impp),可以说是把这些手段用到了极致。
有想了解更多的小伙伴可以加Q群链接里面看一下,希望能够对你们有所帮助。
网友评论