前端性能监控指标与实现
背景
从浏览器底层 api 说起,结合浏览器渲染原理,自底向上谈谈前端性能监控的指标具体都是如何实现的
Navigation Timing 标准
W3C 提供了测试 Web App 性能特征的规范,和时间模型:
浏览器实现
window.performance 对象
浏览器通过 performance
对象实现这一规范。图里的各个指标,可以在 window.performance.timing
对象中拿到。这些字段的值为事件发生时的 UNIX 时间戳
。
事件 | 意义 | 备注 |
---|---|---|
fetchStart |
浏览器准备好使用 http 请求(fetch)文档时 | |
domainLookupStart |
开始 dns 解析时 | 如果用了持续连接(persistent connection),或者信息命中缓存或者本地资源上,将和 fetchStart 相同 |
domainLookupEnd |
dns 解析结束时 | 同上,可能等于 fetchStart |
connectStart |
开始 tcp 连接时 | 如果使用持久链接(persistent connection),将和 fetchStart 相同(同上, 无需 dns 解析) |
secureConnectionStart(可选) |
开始 ssl 安全连接时 | HTTPS 协议有这一步,否则为 0 |
connectEnd |
与服务器建立连接(握手和认证过程全部结束)时 | 同上,可能等于 fetchStart |
requestStart |
向服务器发出 http 请求/读取本地缓存/读取本地资源时 | |
responseStart |
从服务器收到首字节时 | |
responseEnd |
从服务器收到最后一个字节时 | |
domLoading |
开始解析 DOM 树时 | document.readyState = 'loading' ,触发 readystatechange 事件 |
domInteractive |
已经生成 DOM 树时 | document.readyState = 'interactive' ,触发 readystatechange 事件。DOM 对象可以被访问,可以执行例如document.createElement document.body.appendChild |
domContentLoadedEventStart |
所有需要被执行的脚本已经被解析时 | 触发 DOMContentLoaded 事件之前 |
domContentLoadedEventEnd |
所有需要被执行的脚本已经被执行时 | async 的脚本除外 |
domComplete |
HTML 解析完成时 | document.readyState = 'complete' ,触发 readystatechange 事件 |
loadEventStart |
触发 load 事件时 |
还没触发时为0 |
loadEventEnd |
load 事件结束时 |
还没完成时为0 TODO: 是指 load 事件的 handler 函数执行完成吗? |
包括表中所述在内,还有一些导航相关属性:navigationStart、unloadEventStart、unloadEventEnd、redirectStart、redirectEnd 「不再有望成为标准」,未来由
PerformanceNavigationTiming
代替
解析静态资源
在解析 HTML 过程中,对以下静态资源:
- 图片资源:异步下载,不阻塞解析 HTML
- css 资源:异步生成 CSSOM,不阻塞构建 DOM 树,阻塞渲染(合并 DOM 树和 CSSOM 树,之后生成 render 树,计算尺寸、绘制像素,显示在屏幕上),阻塞后续 JS 的执行
- js 资源:根据
script
的defer
和async
两个属性:- 都没有时会阻塞解析,需要等资源下载完成并且执行后,才会继续解析
async
时并行下载资源,不会阻塞解析和执行,下载完成立即执行(并且阻塞当前解析),所以不会严格按照标签的前后顺序执行。如果依赖 DOM 树或者对其他脚本有依赖,可能出错defer
时并行下载资源,不会阻塞解析和执行,会在domInteractive
(生成 DOM 树)之后,domContentLoaded
之前执行。效果就像放在body
最后的script
<script/> 的 async 与 defer
- async 是「异步」,defer 是「延迟」。defer 可以阻止
domContentLoaded
事件直到脚本执行完(TODO: 规范如此,浏览器实现可能不同) - async 无法保证执行顺序和标签顺序一致,defer 可以
- defer 兼容性更好,async 优先级更高。有
<script async defer />
的用法,支持 async 的浏览器会优先生效,否则降级到 defer,同样不阻塞后续解析,体验更好 - ES module TODO: 待补充
DOMContentLoaded、load 事件
DOMContentLoaded 事件会在完成解析 HTML 之后触发。load 需要等所有资源(包括图片、css)都加载完毕后才触发
performance 的其他
PerformanceEntry
对象用来描述性能指标。在加载图片、js、css 资源的时候会生成,也可以手动 mark()。
有 name
、entryType
、startTime
、duration
四个属性。entryType
可以为 paint、resource、mark 等,paint 记录 FP 和 FCP(见下文),resource 就是再加对应 url 的时间了。performance.getEntries()
可以拿到全部的 entry 列表。
性能监控指标
TTFB(time to first byte)
首字节时间responseStart - fetchStart
FP(first paint) 和 FCP(first contentful paint)
FP:首次绘制,首次向屏幕传输像素(比如非默认颜色的背景)
FCP:首次内容绘制,首次向屏幕传输内容
1 | <head> |
performance 已经标准化:
可见 FCP 要比 FP 晚一些(与 document.write
执行的时间相关),两者也可能相同。
此外:
domContentLoadedEventEnd - fetchStart
可以较好表现首屏时间domInteractive | domLoading - fetchStart
可以较好表现白屏时间
TTI(time to intercative)
首次交互时间
Google 定义的指标,不能完全由 performance api 得出,需要满足在 FCP 之后有5秒的时间内没有长任务(超过 50ms 的任务)、不超过两个正在处理的 GET 请求等
异常监控
语言层面
同步异常:try catch 语法
异步异常:遵循 Promise 的最佳实践,写好 reject 和 catch 函数
框架层面
React: Error Boundaries
class component: componentDidCatch 方法
hooks component: 目前还没有等价写法,官方说「plan to add them soon」
浏览器层面
api | 效果 |
---|---|
window.onerror |
可以捕获(同步和异步的)JS 运行时错误 |
window.addEventListener('error') |
可以捕获(同步和异步的)JS 运行时错误,以及(全局的 img、script)资源加载失败 |
window.addEventListener('unhandledrejection') |
可以监听 Promise 抛出的没有被 catch 的错误 |
element.onerror |
捕获单个元素的资源加载错误 |
js error
window.onerror
Promise reject
unhandledrejection 事件
资源加载失败
window.addEventListener(‘error’)
上报方式 TODO:
- image beacon: GET 1px gif(文件体积最小、跨域友好)
- navigator.sendBeacon(异步请求,不影响页面 unload 和加载下一页性能)
性能优化方向
TODO:
参考资料
蚂蚁金服如何把前端性能监控做到极致?
浏览器渲染机制(二)CSS/JS 阻塞 DOM 解析和渲染
如何进行 web 性能监控
Web 指标