背景

从浏览器底层 api 说起,结合浏览器渲染原理,自底向上谈谈前端性能监控的指标具体都是如何实现的

W3C 提供了测试 Web App 性能特征的规范,和时间模型:

img

浏览器实现

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 过程中,对以下静态资源:

  1. 图片资源:异步下载,不阻塞解析 HTML
  2. css 资源:异步生成 CSSOM,不阻塞构建 DOM 树,阻塞渲染(合并 DOM 树和 CSSOM 树,之后生成 render 树,计算尺寸、绘制像素,显示在屏幕上),阻塞后续 JS 的执行
  3. js 资源:根据 scriptdeferasync 两个属性:
    • 都没有时会阻塞解析,需要等资源下载完成并且执行后,才会继续解析
    • 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()。
nameentryTypestartTimeduration 四个属性。
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
2
3
4
5
6
7
8
9
10
11
12
13
14
<head>
<style>
body {
background-color: yellow;
}
</style>
</head>
<body>
<script>
setTimeout(() => {
document.write("123")
}, 200)
</script>
</body>

performance 已经标准化:
img
可见 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 指标