本期作者
陈祥凯
哔哩哔哩高级开发工程师
背景
当我们在面对一些用户反馈的运行问题时,如白屏,页面加载时间过长,页面卡顿等等,如果能够知道客户端的哪些行为导致了问题的出现,那么在定位解决问题上一定会变得事半功倍。
如客户端白屏可能是某个静态资源挂了,页面加载时间过长可能是静态资源的cdn托管服务不稳定,页面卡顿可能是长耗时的js调用导致阻塞了主线程,为了帮助开发快速收集问题出现时客户端所发生的上下文信息,
我在通用埋点sdk中实现了以下两个监控能力,也是本文接下来要分享的两个主题:
加载性能监控
卡顿监控
希望大家通过阅读本文之后可以打造属于自己的前端监控系统用来保证自己应用的用户体验
用户体验
简单来说,用户体验指的是用户使用一个产品的过程中所建立起来的纯主观心理感受。
一般用户能够忍受加载的最长时间在3到8秒,8秒是一个临界值,如果一个页面的加载时间超过4秒,可能会被用户直接关闭,除非Ta一定要打开那个页面。
还有就是在使用过程中页面出现了报错,卡顿。这些因素都会影响到用户的留存率,很明显用户体验关系到用户的增长。
页面加载性能监控
chrome 开发团队提出了一系列用于检测网页性能的指标(核心web指标),什么是核心web指标?
核心 Web 指标是适用于所有网页的 Web 指标子集,每位网站所有者都应该测量这些指标,并且这些指标还将显示在所有 Google 工具中。每项核心 Web 指标代表用户体验的一个不同方面,能够进行实际测量,并且反映
出以用户为中心的关键结果的真实体验。核心 Web 指标的构成指标会随着时间的推移而发生变化。
本文仅简单列举以下指标,如果想进一步了解这些指标的含义,可以访问https://web.dev/learn-core-web-vitals/查看
FCP (First Contentful Paint,首次内容绘制)指标测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间,应控制在1.8s或以内
LCP (Largest Contentful Paint,最大内容绘制)可视区域内可见的最大图像或文本块完成渲染的相对时间,应控制在2.5s或以内
CLS (Cumulative Layout Shift,布局偏移分数)每当一个可见元素的位置从一个已渲染帧变更到下一个已渲染帧时,就发生了布局偏移,应控制在0.1或以下
TTI (Time To Interactive,页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间),应尽可能的将该值降至最低
TBT (Total Blocking Time,总阻塞时间) 指FCP与TTI之间的总阻塞时间,应该尽量控制在300ms以内
我们可以通过Chrome提供的灯塔工具对页面的加载性能进行评分,如下图所示,我们可以看到TBT指标报红,说明页面被阻塞了将近3s
chrome lighthouse 评分规则(https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/)
如果我们只针对自己开发环境的设备跑出来的分数来优化那显然是不够的,理想状态下还应该对所有用户的页面分数进行采样,了解真实用户对 Web 的整体体验如何,如果整体分数很低的话,就算我们本地开发环境分数
跑的再高那也没有多大意义,那么我们该如何拿到真实用户的核心指标页面评分呢?总不能要求用户在本地帮我们跑lighthouse吧,Chrome 提供了一个专门用于性能监控的web API PerformanceObserver(https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver)。
PerformanceObserver 主要用于监测性能度量事件,在浏览器的性能时间轴记录新的 performanceEntry 时会以回调的方式通知observer,PerformanceEntry 对象代表了 performance 时间列表中的单个 metric 数据,通
过PerformanceObserver我们就可以通过编码的方式拿到用户本地真实的metrics了,PerformanceObserver的使用案例网上比较容易找到,我这里就不详细赘述了。
代码实现
控制台打印结果
因纯web环境无法获取Speed Index指标,所以采用TTI来替代,并设置权重 FCP: 10%,LCP: 25%,CLS: 25%,TBT: 30%,TTI: 10%
可以看出评分结果和Chrome Lighthouse测出来的差不多,最后我们将这些数据进行上报,落表的数据我们可以进行自由消费计算(平均数,分位数)并制定一个基准线,当分数过低时进行告警
卡顿监控
假设在某一个公司内部后台系统中,使用方向我们的后台开发人员反馈页面卡顿,但是使用方和开发同学不在一个办公地点,线上沟通能够得到的信息少之又少。
使用方:大佬,这个页面用起来好卡,帮忙看一下,
开发同学:我这边本地运行看了一下不卡啊,很正常啊。
其他沟通细节略过...
最后来回沟通获取到的信息还是没能定位到问题所在,还消耗了大量的时间成本。
其实这都很正常,毕竟办公环境不同,设备,网络快慢等等因素都可能会造成页面卡顿的。
那么我们有办法监控到卡顿吗?其实是可以的,我们依然可以通过上文提到的 PerformanceObserver API来实现,在介绍如何捕获卡顿之前需要先搞明白一个概念 长任务 (Longtask),什么是长任务?
长任务是指长时间(大于等于50ms)独占主线程,导致界面卡顿的 JavaScript 代码,
比如以下常规场景:
长耗时的事件回调(long running event handlers)
代价高昂的回流和其他重绘(expensive reflows and other re-renders)
浏览器在超过 50 毫秒的事件循环的相邻循环之间所做的工作(work the browser does between different turns of the event loop that exceeds 50 ms)
当我们执行以下代码时,通过Chrome录制可以看到一个持续时间为3s的Longtask
那我们怎么通过编码的方式来获取Longtask呢?PerformanceObserver提供了对Longtask的监控能力
通过 PerformanceObserver 监听 longtask ,一旦出现了阻塞主线程的长任务,通过回调通知我们可以拿到 PerformanceLongtaskTiming(https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongtaskTiming)对象
其中duration字段表示此次长任务的持续时长,单位时间毫秒,可以看到值为3000,与Chrome控制台录制里的 Longtask 3.00s的结果 保持一致,
startTime字段表示卡顿开始的时间(DOMHighResTimeStamp:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMHighResTimeStamp),这样就可以在每次长任务触发回调的时候进行上报,在落表数据中我们就能统计出应用的卡顿时长和次数了。
卡顿链路回溯
单单统计卡顿时长和次数显然是不足以帮助开发同学快速定位解决问题的,只能说现阶段我们可以不依赖使用方的反馈就能感知到哪些应用出现了卡顿,仅此而已。如果我们能还原卡顿前的用户行为(网络请求,用户触发的操作(click, keydown)),那这样是不是在定位问题上会变的更容易,我们可以很明确的知道是哪个请求或者用户点击了哪个dom节点导致了长任务的出现。要想监听用户触发的click和keydown事件很简单这里就不讲了,那网络请求如何拦截呢?一种方式是使用ajax-hook,原理是重写了XMLHttpRequest,侵入性较强,如果业务方在某处也对XMLHttpRequest进行重写,可能会出现一些无法预估的问题而导致出现bug,还依赖jqurey,代码体积也会增加,而且无法拦截fetch以及静态资源的加载,需要再另外实现,所以直接就pass了。最后我采用的方式是使用PerformanceObserver来监听resource实现对网络请求的收集,也是比较合理的一种方式,跟Longtask一样,每当network发起一次网络请求,回调会通知返回PerformanceResourceTiming对象集合
最后我们可以通过startTime,endTime(startTime + duration)来过滤出Longtask前任意时间的Timings,然后进行关联到某一个长任务的上报。
获取调用栈信息
虽然现在已经可以复现卡顿前的用户行为,但有时候一次交互中可能会调用n个函数的执行,我们还需要排查是哪个函数的执行导致页面出现了卡顿,当一个应用逻辑足够复杂时心智负担无疑是巨大的,如果我们能在每次长任务触发时能够统计到调用栈以及执行时间,如下图Chrome录制,可以知道点击时调用了testClick函数,执行时间为3000.3ms
显然我们是不可能让用户去跑Chrome录制,然后发Profiler文件给我们的,不够灵活。
接下来介绍的API可以帮助我们实现调用栈的收集
js-self-profiling API
文档地址:https://wicg.github.io/js-self-profiling
该规范描述了一个API,允许Web应用程序可以测量客户端JavaScript的执行时间,因安全问题浏览器默认是不开启的,
需要在响应头中添加 Document-Policy: js-profiling=?1 开启
使用方式
trace interface
根据上述接口定义的数据结构,我们可以按下图的步骤获取到一个完整的调用栈信息
每个调用栈我们也可以把它抽象成一个timing,第一个sample的timestamp即startTime,runningTime表示duration,对数据进行以下处理后进行上报
对timing samples按startTime进行排序(从小到大升序)
过滤出longtask结束时间前的timing samples
过滤出开始时间小于等于longtask开始前10s(需要回溯的时间)的timing samples
最终效果
当点击按钮的时候会执行以下testClick函数
埋点sdk捕获到的卡顿信息
根据上报数据的 timing samples 绘制瀑布图,可以很明显的看到点击按钮之后发起了一个请求,请求响应后调用testClick函数导致了页面卡顿
查看点击dom节点的选择器
查看网络请求的url,状态码,数据大小
查看调用函数的名称,资源地址以及行和列
我们还可以通过添加source-map来反解查看源码
对上报数据进行了一波分析之后,发现有些数据还是无法定位到具体的调用栈,后来发现ProfileSample的定义中stackId是可选的,就说明有些采样数据是没有stackId的,那这些数据又是什么呢?
实际上这些数据可以理解为浏览器系统底层的调用,比如gc,script解析,页面回流重绘等等..,如果我们没有开启跨域隔离的话,是没法额外显示这些标记信息的,开启之后ProfilerSample中会增加mark字段用来标记类型
因开启跨域隔离之后业务需要牵扯到的改动过于庞大,所以就只能先放弃系统调用栈的跟踪了。
既然plan A不行,那肯定得思考plan B了,最后想到了使用web录屏来暂时代替无法定位到系统调用栈的问题
web录屏
实现思路
使用rrweb对页面卡顿发生前的dom节点变化进行分段式录制,rrweb(https://github.com/rrweb-io/rrweb)全称 record and replay the web,是当下很流行的一个录制屏幕的开源库,
record配置提供了 checkoutEveryNms 字段来进行分批录制
如果我们要录制卡顿发生前10s的内容,可以这样实现
展望未来
Chrome 115版本会引入 ComputePressure API(https://github.com/w3c/compute-pressure/blob/main/HOWTO.md),用于公开 CPU 负载计算。
这样就可以对客户端的CPU使用率进行监控来保证应用的健康度,当发现应用的CPU使用率在连续一个时间段高于某一峰值时就触发告警,也可以像卡顿链路回溯一样绑定上下文 timings数据来分析CPU异常归因。
在未来,这个功能还可能会被扩展到显示包括温度和电池状态等内容。
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路