前端监控系列4 | SDK 体积与性能优化实践( 三 )

通常这类 benchmark 工具都是在 Node 上执行的,但是我们的 SDK 是个前端监控 SDK,依赖了非常多的浏览器环境对象,我们几乎不可能在 Node 环境去创造或模拟这些对象,我们有没有办法在浏览器里去运行这段脚本,做性能自动化测试呢?
利用 Puppeteer 在浏览器环境中执行 Benchmark由于我们的前端监控依赖浏览器环境,我们可以将上述 benchmark 测试代码打包成 commonjs 之后放入 headless chrome 浏览器中执行,并通过 puppeteer 收集执行结果 。

Puppeteer 是一个 Node 模块,提供了通过 Devtool Protocol 控制 Chrome 或者 Chromium 的能力 。Puppeteer 默认运行 Chrome 的无头版本,也可以通过设置运行 Chrome 用户界面版 。
下面是一段方便理解操作 puppeteer 过程的伪代码,仅作参考,实际情况较为复杂,需要等待未完成的异步请求等:
const browser = await puppeteer.launch()const page = await browser.newPage()const cdp = await page.target().createCDPSession()// 用于 benchmark 脚本和 puppeteer 之间的通信,用以收集结果await page.evaluate(() => (window.benchmarks = []))// 将 pushResult 方法暴露给浏览器,来将结果收集到 node 端await page.exposeFunction('pushResult',(result: any) => benchmark.results.push(result))await cdp.send('Profiler.enable')await cdp.send('Profiler.start')// 开始执行 benchmarkawait page.addScriptTag({content: file.toString(),})await Promise.race([timeout, allBenchmarksDone()])// profile 可用于绘制火焰图const { profile } = await cdp.send('Profiler.stop')await page.close()通过运行以上脚本,我们便可以在无头浏览器中运行我们的性能测试脚本,在测试脚本产出结果后添加调用 pushResult 方法来收集测试结果 。在实际的 benchmark 测试中,我们发现开启性能监听(即运行各个性能监控的 PerformanceObserver.observe 方法)最大耗时达到了21ms,虽然看上去并不久,但若和其他监听同时执行,加上引入业务代码的复杂性和移动端更弱的 CPU 性能,极有可能成为给业务带来 longtask 的罪魁祸首 。性能监控性能成为了瓶颈 。接下来,我们将性能监听一个个拆分,用同样的方式单独测试每一个性能监听的耗时 。在实际的 benchmark 结果中,我们发现 fp、fcp、lcp、cls 监控耗时最大,加在一起超过了10ms,占了一半以上,是我们之后需要重点优化的地方 。
除此之外利用 puppeteer 的能力,我们不仅可以得到 benchmark 的结果,还可以获取到整个 benchmark 过程的 profile 数据,利用 speedscope 绘制出函数执行过程中的火焰图:
绘制火焰图的具体实现不在本文讨论范围内,感兴趣的同学可以参考 speedscope 官方文档

前端监控系列4 | SDK 体积与性能优化实践

文章插图
此处显示的时间为该用例执行总耗时(单次耗时*次数)
如何衡量异步任务性能?Benny 的 api 是支持异步测试用例的,测量的是每个异步函数从开始执行到 resolve 的时间 。但通常这并不是我们想要的衡量的数据,因为异步任务的执行过程中并不是一直占据着主线程 。对于一些异步的定时任务(例如 SDK 的崩溃检测、卡顿检测、白屏检测),将他们拆解为一系列可测的同步任务能更直观的展示各个阶段的性能耗时 。例如我们 SDK 的前端白屏检测,由一个 mutationObserver 和触发白屏检测的函数组成 。我们可以单独对 mutationObserver 的回调和触发函数做性能衡量 。这两个方法已没有很好的优化方式了 。但是根据 benchmark 结果并结合源码可以发现,性能监控所有指标项的开启均为同步执行,每一项指标都会对页面做事件监听或者 PerformanceObserver 监听,且这些原生监听耗时都在毫秒级 。于是我们对性能做了如下优化:

经验总结扩展阅读