
背景
近期主要工作内容是进校开放平台(简称开平)相关业务,开平简单来说就是一个可为第三方应用提供接入主端(例如微信、飞书)应用能力的平台,为了让第三方应用稳定可靠地接入开平,需要为其提供一些底层的基础能力,其中这是不可或缺的一部分。目前,如何在学校开平进行三方应用的监控管理仍处于初步研究阶段,了解前端监控的相关背景知识。鉴于我们公司有一套非常完美的 APM 因此,以下理论和源代码参考自我公司 APM Web SDK 源码。
监控流程
:明确需要采集的指标和方法。
:以一定的策略报告上一步收集的数据。
:接收报告数据后,服务端需要清理和存储数据。
:数据最终会相似 Slardar Web 该监控平台以图表等形式进行可视化显示,并提供监控报警等消费能力。
上述过程似乎并不复杂,但每个环节都有很多技术细节。本文主要关注前端视角下的数据采集和报告。
数据采集
前端监控的第一步是明确哪些数据值得我们收集。前端环境下的监控数据可分为环境信息,数据和数据:
环境信息
收集的监控数据通常设置一些通用的环境信息,可以。下图列出了一些常见的环境信息:
异常监控
JS 异常
Script Error
先抛开如何收集 JS 更不用说异常信息了,如果你甚至在收集之前报告了错误的信息,那么即使收集到这些数据也是无效的。巧合的是,确实有这样一个场景:当页面加载不同域的脚本(例如页面) JS 托管在 CDN)当语法错误发生时,浏览器不会根据安全机制给出语法错误的细节,而是简单的 Script error. 。
因此,如果你想监控你页面上的详细错误信息 SDK 你需要在页面上捕获脚本 script 添加 crossorigin= anonymous 属性,脚本的服务设置 CORS 响应头 Access-Control-Allow-Origin: * ,这是 JS 异常监测的第一个准备工作。
编译和操作错误
常见的 JS 错误可分为编译错误和操作错误在 IDE 层面会给出提示,一般不会流入线上,所以编译中的错误不在监控范围内。
有同学说在 Slardar 上时常看到 SyntaxError 这种情况通常是 JSON.parse 由于浏览器误或浏览器兼容性问题,操作错误不是编译错误。
我们主要关注异常监控 JS ,大多数场景的处理方法如下:
| :自行感知的 | try-catch 错误报告后 |
| :没有手动 catch 异常运行(包括异步但) | 通过 window.onerror进行监听 |
| :自行感知的 promise 异常 | promise catch 捕获后,报告错误 |
| :没有手动 catch 的 promise 异常 | 监听 window 对象的 unhandledrejection 事件 |
总的来说,监控 SDK 它将帮助用户捕获他们没有自我感知的异常并报告整体情况,并通常为自我捕获的异常提供手动报告界面。
SourceMap
假设现在已经收集到页面的存在 JS 当然,当你最终消费时,你我们想看到的是错误的初始来源和调用堆栈,但实际上是错误的 JS 代码经过各种转换混淆压缩,早已面目全非。因此,它需要在包装阶段生成 SourceMap 对原始报错信息进行反向分析。
以 Sentry (Slardar 也有用到 Sentry)例如,一般流程如下:
收集侧向监控平台服务端收集错误信息。
接入业务方自行上传 SourceMap 文件到监控平台服务端,上传后删除本地文件 SourceMap打包后的文件 js 文件末尾不需要 SourceMap URL,尽量避免 SourceMap 泄漏。
服务端通过 source-map 工具结合 SourceMap 将原始错误信息定位到源代码的具体位置。
静态资源加载异常
捕获静态资源加载异常有两种方法:
静态资源加载异常元素
onerror处理方法。
异常触发资源加载
error事件不会起泡,所以window.addEventListener('error', cb,)捕获在事件捕获阶段。
第一种方法侵入性太强,不够优雅。目前主流方案采用第二种方式监控:
捕获静态资源加载异常
APM 平台通常有静态资源加载的所有细节,其原理是通过 PerformanceResourceTiming API 来采集静态资源加载的基本情况,这里不做展开。
请求异常
业务中的 AJAX 请求或者 Fetch 要求在不同的网络环境或客户端环境中表现不稳定。我们很难通过本地测试来测试或感知这些不稳定的情况,所以我们需要 HTTP 要求在线监控,通过 HTTP 采集错误日志,然后进行一系列的分析和监控。
请求异常通常是指 HTTP 请求失败或者 HTTP 请求返回的状态码不是 20X。
那么如何要求异常监控呢?一般的方法是原生的 XMLHttpRequest 对象和 fetch 重写方法,实现代理对象中状态码的监控和错误报告:
重写 XMLHttpRequest 对象
重写 fetch 方法
当然,重写上述方法后,除了可以监控异常请求外,还可以自然收集正常响应的请求状态,如 Slardar 将分析所有报告请求的持续时间,以获得慢请求的比例:
PS:如果通过 XHR 或 fetch 如果上报监控数据,上报请求也会被拦截,可以有选择地进行一层过滤。
卡顿异常
卡顿是指显示器刷新时下一帧的画面尚未准备好,导致同一帧连续多次显示,让用户感觉页面不流畅,即所谓的帧丢失。我们熟悉衡量页面是否卡住的指标 FPS。
如何获取 FPS
Chrome DevTool 中有一栏 Rendering 中包含 FPS 但目前浏览器标准尚未提供相应的指标 API ,只能手动实现。这里需要帮助。 requestAnimationFrame 实现方法模拟,览器会在下一次重绘之前执行 rAF 的回调,因此可以通过 。
通过 rAF 计算 FPS
如何上报“真实卡顿”
从技术角度看 FPS 低于 60 即视为卡顿,但在真实环境中用户很多行为都可能造成 FPS 的波动,并不能无脑地把 FPS 低于 60 以下的 case 全部上报,会造成非常多无效数据,因此需要结合实际的用户体验重新定义“真正的卡顿”,这里贴一下司内 APM 平台:
:当前页面连续 3s FPS 低于 20。
:当用户进行交互行为后,渲染新的一帧的时间超过 16ms + 100ms。
崩溃异常
Web 页面崩溃指在网页运行过程页面完全无响应的现象,通常有两种情况会造成页面崩溃:
JS 主线程出现无限循环,触发浏览器的保护策略,结束当前页面的进程。
内存不足
发生崩溃时主线程被阻塞,因此对崩溃的监控只能在独立于 JS 主线程的 Worker 线程中进行,我们可以,如果主线程崩溃,就不会有任何响应,那就可以。这里继续贴一下 Slardar 的检测策略:
JS 主线程:
固定时间间隔(2s)向 Web Worker 发送心跳
Web Worker:
定期(2s)检查是否收到心跳。
超过一定时间(6s)未收到心跳,则认为页面崩溃。
检测到崩溃后,通过 http 请求进行异常上报。
崩溃检测
性能监控
性能监控并不只是简单的监控“页面速度有多快”,需要从用户体验的角度全面衡量性能指标。(就是所谓的 )目前业界主流标准是 Google 最新定义的 Core Web Vitals:
:
:
:
可以看到最新标准中,以往熟知的 FP、FCP、FMP、TTI 等指标都被移除了,个人认为这些指标还是具备一定的参考价值,因此下文还是会将这些指标进行相关介绍。(谷歌的话不听不听🙉)
Loading 加载
和 Loading 相关的指标有 FP 、FCP 、FMP 和 LCP,首先来看一下我们相对熟悉的几个指标:
FP/FCP/FMP
一张流传已久的图
当前页面,通常将开始访问 Web 页面的时间点到 FP 的时间点的这段时间视为,简单来说就是有屏幕中像素点开始渲染的时刻即为 FP。
当前页面的时间点,这里的 内容 通常指的是文本、图片、svg 或 canvas 元素。
这两个指标都通过 PerformancePaintTiming API 获取:
通过 PerformancePaintTiming 获取 FP 和 FCP
下面再来看 FMP 的定义和获取方式:
表示的时间,在这个时刻,页面整体布局和文字内容全部渲染完成,用户能够看到页面主要内容,产品通常也会关注该指标。
FMP 的计算相对复杂,因为浏览器并未提供相应的 API,在此之前我们先看一组图:
从图中可以发现页面渲染过程中的一些规律:
在 1.577 秒,页面渲染了一个搜索框,此时已经有 60 个布局对象被添加到了布局树中。
在 1.760 秒,页面头部整体渲染完成,此时布局对象总数是 103 个。
在 1.907 秒,页面主体内容已经绘制完成,此时有 261 个布局对象被添加到布局树中从用户体验的角度看,此时的时间点就是是 FMP。
可以看到布局对象的数量与页面完成度高度相关。业界目前比较认可的一个计算 FMP 的方式就是——「页面在加载和渲染过程中即为当前页面的 FMP 」
实现原理则需要通过 MutationObserver 监听 document 整体的 DOM 变化,在回调计算出当前 DOM 树的分数, 。
至于如何计算当前页面 DOM 🌲的分数,LightHouse 的源码中会根据当前节点深度作为变量做一个权重的计算,具体实现可以参考 LightHouse 源码。
const curNodeScore = 1 + 0.5 * depth;
const domScore = 所有子节点分数求和
上述计算方式性能开销大且未必准确,LightHouse 6.0 已明确废弃了 FMP 打分项,建议在具体业务场景中根据实际情况手动埋点来确定 FMP 具体的值,更准确也更高效。
LCP
没错,LCP (Largest Contentful Paint) 是就是用来代替 FMP 的一个性能指标 ,可以用来确定页面的主要内容何时在屏幕上完成渲染。
使用 Largest Contentful Paint API 和 PerformanceObserver 即可获取 LCP 指标的值:
获取 LCP
Interactivity 交互
TTI
TTI(Time To Interactive) 表示 TTI 值越小,代表用户可以更早地操作页面,用户体验就更好。
这里定义一下什么是:
页面已经显示有用内容。
页面上的可见元素关联的事件响应函数已经完成注册。
事件响应函数可以在事件发生后的 50ms 内开始执行(主线程无 Long Task)。
TTI 的算法略有些复杂,结合下图看一下具体步骤:
TTI 示意图
Long Task: 阻塞主线程达 50 毫秒或以上的任务。
从 FCP 时间开始,向前搜索一个不小于 5s 的静默窗口期。(定义:窗口所对应的时间内没有 Long Task,且进行中的网络请求数不超过 2 个)
找到静默窗口期后,从静默窗口期向后搜索到最近的一个 Long Task,Long Task 的结束时间即为 TTI。
如果一直找到 FCP 时刻仍然没有找到 Long Task,以 FCP 时间作为 TTI。
其实现需要支持 Long Tasks API 和 Resource Timing API,具体实现感兴趣的同学可以按照上述流程尝试手动实现。
FID
FID(First Input Delay) 用于度量,是用户第一次与页面交互到浏览器真正能够开始处理事件处理程序以响应该交互的时间。
其实现使用简洁的 PerformanceEventTiming API 即可,回调的触发时机是用户首次与页面发生交互并得到浏览器响应(点击链接、输入文字等)。
获取 FID
至于为何新的标准中采用 FID 而非 TTI,可能存在以下几个因素:
FID 是的,只有用户进行了交互动作才会上报 FID,TTI 不需要。
FID 用户对页面交互性和响应性的,良好的第一印象有助于用户建立对整个应用的良好印象。
Visual Stability 视觉稳定
CLS
CLS(Cumulative Layout Shift) 是对在页面的整个生命周期中发生的每一次意外布局变化的的度量,。
听起来有点复杂,这里做一个简单的解释:
不稳定元素:一个但发生较大偏移的可见元素称为
布局变化得分:元素从原始位置偏移到当前位置影响的页面比例 * 元素偏移距离比例
举个例子,一个占据页面高度 50% 的元素,向下偏移了 25%,那么其得分为 0.75 * 0.25,大于标准定义的 0.1 分,该页面就视为视觉上没那么稳定的页面。
使用 Layout Instability API 和 PerformanceObserver 来获取 CLS:
获取 CLS
一点感受:在翻阅诸多参考资料后,私以为性能监控是一件长期实践、以实际业务为导向的事情,业内主流标准日新月异,到底监控什么指标是最贴合用户体验的我们不得而知,对于 FMP、FPS 这类浏览器未提供 API 获取方式的指标花费大量力气去探索实现是否有足够的收益也存在一定的疑问,但毋容置疑的是从自身页面的业务属性出发,结合一些用户反馈再进行相关手段的优化可能是更好的选择。(更推荐深入了解浏览器渲染原理,写出性能极佳的页面,让 APM 同学失业
数据上报
得到所有错误、性能、用户行为以及相应的环境信息后就要考虑如何进行数据上报,理论上正常使用ajax 即可,但有一些数据上报可能出现在页面关闭 (unload) 的时刻,这些请求会被浏览器的策略 cancel 掉,因此出现了以下几种解决方案:
优先使用 Navigator.sendBeacon,这个 API 就是为了解决上述问题而诞生,它通过 HTTP POST 将数据异步传输到服务器且不会影响页面卸载。
如果不支持上述 API,动态创建一个 的方式传递。
使用 进行上报以延迟页面卸载,不过现在很多浏览器禁止了该行为。
Slardar 采取了第一种方式,不支持 sendBeacon 则使用 XHR,偶尔丢日志的原因找到了。
由于监控数据通常量级都十分庞大,因此不能简单地采集一个就上报一个,需要一些优化手段:
:将多条数据聚合一次性上报可以减少请求数量,例如我们打开任意一个已接入 Slardar 的页面查看
batch 请求的请求体:
像崩溃、异常这类数据不出意外都是设置 100% 的采样率,对于自定义日志可以设置一个采样率来减少请求数量,大致实现思路如下:
总结
本文旨在提供一个相对体系的前端监控视图,帮助各位了解前端监控领域我们能做什么、需要做什么。此外,如果能对页面性能和异常处理有着更深入的认知,无论是在开发应用时的自我管理(减少 bug、有意识地书写高性能代码),还是自研监控 SDK 都有所裨益。
如何设计监控 SDK 不是本文的重点,部分监控指标的定义和实现细节也可能存在其他解法,实现一个完善且健壮的前端监控 SDK 还有很多技术细节,例如每个指标可以提供哪些配置项、如何设计上报的维度、如何做好兼容性等等,这些都需要在真实的业务场景中不断打磨和优化才能趋于成熟。