前端性能监控主要分为两种方式,一种叫做合成监控(Synthetic Monitoring,SYN),另一种是真实用户监控(Real User Monitoring,RUM)。合成监控一般会借助工具来做检查例如:lighthouse。而真实用户监控的时候,我们就需要采集一些用户数据。
关于用户数据采集,我们通常会考虑几个问题:
- 使用标准 API 收集数据
- 定义合适的指标
- 上报数据,分析数据
本文我们先主要介绍如何收集数据和定义核心性能指标。
数据采集方式
利用 performance API 我们可以轻松的的获取页面的相关性能数据和衡量一些流程是时间。
例如:
- 高精度时间 performance.now() 该事件不受系统时间影响,是一个单调递增的时间。这样就可以保证,我们统计某一阶段的耗时情况精确且不受宿主环境影响
- timing API时序图,他可以帮助我们获取页面各阶段的相关性能时间: 具体含义就不介绍了,网上有很多
- Performance Timeline Level2 提供了更多的扩展功能有兴趣的可以看看
定义合适的指标
有了采集方式,我们需要一个合适的指标来衡量我们页面的性能,这里介绍几个核心指标
起始时间
首先从页面加载性能开始,我们既然要统计各阶段的耗时,就需要一个起始时间。我们来看下几个可以作为起始的时间点:
- navigationStart:可以理解为输完url,按下回车的那一刹那
- fetchStart:是指在浏览器发起任何请求之前的时间值。
- requestStart:代表浏览器发起请求的时间节点,请求的方式可以是请求服务器、缓存、本地资源等。
对于 navigationStart 和 fetchStart,在当前文档为空的情况下,navigationStart = fetchStart。这中间,还有一个 redirect 重定向时间的耗时,大部分情况下,这个差值在10毫秒以内,即不存在重定向。
这里我们通常会把 navigationStart 作为起始时间,例如:
首字节时间(是指在 TCP 协议的三次握手之后,浏览器开始发出 HTTP 请求,然后服务器响应时回复的第一个字节)即为
performance.timing.responseStart - performance.timing.navigationStart
FCP (First Contentful Paint)
浏览器从响应用户输入网址地址,到浏览器开始渲染内容的时间,可以直接的反映网站性能的优劣,在衡量网站性能的过程中更靠近用户。我们可以通过 performance.getEntriesByType(‘paint’)[1] 来获得这个时间:
FMP (First Meaning Paint)
首次有效绘制(主要内容绘制完成时间),这个时间是无法从浏览器的 API 中获得的,需要我们自己来衡量。通常这个时间也是我们用来评估用户感知上的白屏时间。我们计算该时间采用的算法是阿里云提出的一个算法:
详情见 GMTC 大前端监控的最佳实践
然而他并没有提供具体的实现代码,按照他的思路我们来自己实现一套。
页面组成部分
首先我们要确定页面的重要组成部分(也就是可是范围内,用户感知上重要的内容),我们规定这部分节点需要满足如下特点:
- 节点元素体积大,在首屏内面积占比大
- 节点是重要的元素组成,比如图片,视频等内容
所以我们设定一个得分系统,来确定每个节点的得分,得分越高说明改部分节点越重要
|
|
收集页面信息,计算各部分节点得分
有了标准,我们就要开始收集在页面加载过程中,出现的页面各节点的信息:节点渲染的时间(用于统计最后的 FMP 时间),节点的得分(用于决定影响 FMP 时间的占比)。这里我们通过监听 MutationObserver 来做收集。
|
|
这里 setTag 采用深度优先遍历来标记节点,标记条件为:
- 该节点在首屏可视区内
- 该节点面积不为 0
如果父节点不满足调节,则不再继续向下遍历,深搜可以帮我们节约大量标记时间。
|
|
当页面的 document.readyState 为 complete 或,load 事件触发后,我们停止 MutationObserver 监听,开始计算关键节点和 FMP 时间。
根据标记好的信息,计算各节点的的分情况
这里我们访问我们标记好的节点。节点得分与关键影响因素计算过程如下:
- 当前节点得分 = 节点可视面积 * 权重 weight;关键影响因素为节点元素本身
- 计算当前节点下所有子节点的得分,如果得分综合大于当前节点得分。则更新当前节点得分为所有子节点得分,当前节点的关键影响因素记为所有子节点。
- 依照这个规则,递归出所有标记节点的得分和影响因素。
- 页面根节点的得分,与关键影响因素即为最后结果
|
|
这样我们就有了关键节点了,通过计算关键节点的最晚渲染时间,及为 FMP 时间。
这个时间计算方式如下:
如果关键节点为普通元素(weight === 1),那么元素上的 fmp_c 的标记时间即为该节点的渲染时间。(上文 this.statusCollector 中查找即可)
如果该节点是资源类型 (img,video)等,那我们我们可以取出资源的 url 地址,在 performance.getEntries() 中查到对应资源的加载时间。
对最后得出的所有关键节点取得分平均值,去掉平均值以下的数据;剩余数据即为有效数据,在这些有效数据中,取最晚加载渲染的元素时间即为最后的 FMP 时间
完整代码:FMP 计算实现脚本
FST (Fisrt Screen Time)
首屏时间是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。对于该时间这里我们有如下定义:
如果页面首屏有图片:
首屏时间 = 首屏图片全部加载完毕的时刻 - window.performance.timing.navigationStart
如果首页没有图片:
首屏时间 = 页面处于稳定状态前最后一次 dom 变化的时刻 - window.performance.timing.navigationStart
具体实现方案,我们可以参考这篇文章:如何自动获取首屏时间
TTI (Time to interactive)
TTI 应该衡量的是用户可交互的时间,通常我们将 domInteractive
的时间作为用户可交互时间,这个时间仅仅代表的是 HTML 构建完成(早于 dom ready)时间,此时并不意味着用户真的可以交互。
例如:
- 在 react 下,我们认为的用户可交互时间,应当是主页面 componentDidMount 事件触发的数据加载完成后,页面 componentDidUpdate 后的时间点为用户可交互时间。而在这之前,页面其实就只有一个 div#root 空节点。
- 在骨架屏下,骨架屏渲染完毕后,也并不意味着页面可交互
所以有的时候,业务自己定义的一个时间点,来上报这部分数据。如果借助自动化监测的话,我觉得靠谱的方案是 Google 提出的 TTI 出现时间:
- 页面已经渲染了可用的内容,FCP 时间点之后(或者 FMP)
- 可见元素已经完成了事件注册
- 页面可在 50 毫秒内响应用户交互。
相关资料:
有一点需要注意的是,这个方案有兼容性问题,需要 chrome 版本大于 58
FPS (Frames Per Second)
FPS 多用于动画流畅度,交互流畅度等。所以实时监控 FPS 的做法,可能并无意义。当页面长时间挂着且几乎没有交互的时候(例如接单页)等,页面 FPS 实时数据均为 60 FPS。这样就导致了上报上来的数据毫无意义。
所以统计 FPS 性能指标,需要结合用户的实际操作才有意义。有一种采集 FPS 的做法,是在可能的交互上都做 FPS 监控,一旦 FPS 连续几个点低语一定的阈值,则上报。
在业务上,我们通常会监听滚动 FPS 这一指标,计算滚动 FPS 的方法如下:
|
|
API 指标
API 的指标口径主要为 API 的平均延迟,API 的成功率(网络成功率 & 业务成功率),为了方便上报这部分数据,我们通常会通过重写 xhr.send 和 xhr.open 的当时来实现。
|
|
通常我们会上报的 API 相关数据有:
- url:请求的 url
- status:业务状态码(标识业务的成功率)
- responseTime:API 请求耗时
- content:请求附加的额外信息,比如请求错误原因等
- networkCode:网络状态码 (标识网络成功率)
这种方式仅限于 ajax 请求,如果你用了 fetch 那还是自己写一个拦截吧。