卡顿、网络环境指标采集

如何监控网页性能

Posted by Li Yucang on January 5, 2024

卡顿、网络环境指标采集

卡顿指标采集

所谓卡顿,简单来说就是页面出现卡住了的不流畅的情况。 提到它的指标,你是不是会一下就想到 FPS(Frames Per Second,每秒显示帧数)?FPS 多少算卡顿?网上有很多资料,大多提到 FPS 在 60 以上,页面流畅,不卡顿。但事实上并非如此,比如我们看电影或者动画时,素虽然 FPS 是 30 (低于60),但我们觉得很流畅,并不卡顿。

FPS 低于 60 并不意味着卡顿,那 FPS 高于 60 是否意味着一定不卡顿呢?比如前 60 帧渲染很快(10ms 渲染 1 帧),后面的 3 帧渲染很慢( 20ms 渲染 1 帧),这样平均起来 FPS 为95,高于 60 的标准。这种情况会不会卡顿呢?实际效果是卡顿的。因为卡顿与否的关键点在于单帧渲染耗时是否过长。

但难点在于,在浏览器上,我们没办法拿到单帧渲染耗时的接口,所以这时候,只能拿 FPS 来计算,只要 FPS 保持稳定,且值比较低,就没问题。它的标准是多少呢?连续 3 帧不低于 20 FPS,且保持恒定。

以 H5 为例,H5 场景下获取 FPS 方案如下:

var fps_compatibility= function () {
    return (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 1000 / 60);
        }
    );
}();
var fps_config={
  lastTime:performance.now(),
  lastFameTime : performance.now(),
  frame:0
}
var fps_loop = function() {
    var _first =  performance.now(),_diff = (_first - fps_config.lastFameTime);
    fps_config.lastFameTime = _first;
    var fps = Math.round(1000/_diff);
    fps_config.frame++;
    if (_first > 1000 + fps_config.lastTime) {
        var fps = Math.round( ( fps_config.frame * 1000 ) / ( _first - fps_config.lastTime ) );
        console.log(`time: ${new Date()} fps is:`, fps);
        fps_config.frame = 0;    
        fps_config.lastTime = _first ;    
    };           
    fps_compatibility(fps_loop);   
}
fps_loop();
function isBlocking(fpsList, below=20, last=3) {
  var count = 0
  for(var i = 0; i < fpsList.length; i++) {
    if (fpsList[i] && fpsList[i] < below) {
      count++;
    } else {
      count = 0
    }
    if (count >= last) {
      return true
    }
  }
  return false
}

利用 requestAnimationFrame 在一秒内执行 60 次(在不卡顿的情况下)这一点,假设页面加载用时 X ms,这期间 requestAnimationFrame 执行了 N 次,则帧率为1000* N/X,也就是FPS。

由于用户客户端差异很大,我们要考虑兼容性,在这里我们定义 fps_compatibility 表示兼容性方面的处理,在浏览器不支持 requestAnimationFrame 时,利用 setTimeout 来模拟实现,在 fps_loop 里面完成 FPS 的计算,最终通过遍历 fpsList 来判断是否连续三次 fps 小于20。

如果连续判断 3次 FPS 都小于20,就认为是卡顿。

卡顿链路回溯

单单统计卡顿时长和次数显然是不足以帮助开发同学快速定位解决问题的,只能说现阶段我们可以不依赖使用方的反馈就能感知到哪些应用出现了卡顿,仅此而已。

如果我们能还原卡顿前的用户行为(网络请求,用户触发的操作(click, keydown)),那这样是不是在定位问题上会变的更容易,我们可以很明确的知道是哪个请求或者用户点击了哪个dom节点导致了长任务的出现。

要想监听用户触发的click和keydown事件很简单这里就不讲了,那网络请求如何拦截呢?一种方式是使用ajax-hook,原理是重写了XMLHttpRequest,侵入性较强,如果业务方在某处也对XMLHttpRequest进行重写,可能会出现一些无法预估的问题而导致出现bug,代码体积也会增加,而且无法拦截fetch以及静态资源的加载,需要再另外实现,所以直接就pass了。

最后我采用的方式是使用PerformanceObserver来监听resource实现对网络请求的收集,也是比较合理的一种方式,跟Longtask一样,每当network发起一次网络请求,回调会通知返回PerformanceResourceTiming对象集合

最后我们可以通过startTime,endTime(startTime + duration)来过滤出Longtask前任意时间的Timings,然后进行关联到某一个长任务的上报。

获取调用栈信息

虽然现在已经可以复现卡顿前的用户行为,但有时候一次交互中可能会调用n个函数的执行,我们还需要排查是哪个函数的执行导致页面出现了卡顿,当一个应用逻辑足够复杂时心智负担无疑是巨大的,如果我们能在每次长任务触发时能够统计到调用栈以及执行时间,如下图Chrome录制,可以知道点击时调用了testClick函数,执行时间为3000.3ms

显然我们是不可能让用户去跑Chrome录制,然后发Profiler文件给我们的,不够灵活。

web录屏

使用rrweb对页面卡顿发生前的dom节点变化进行分段式录制,rrweb()全称 record and replay the web,是当下很流行的一个录制屏幕的开源库,

record配置提供了 checkoutEveryNms 字段来进行分批录制

如果我们要录制卡顿发生前10s的内容,可以这样实现

展望未来

Chrome 新版本会引入 ComputePressure API(),用于公开 CPU 负载计算。

这样就可以对客户端的CPU使用率进行监控来保证应用的健康度,当发现应用的CPU使用率在连续一个时间段高于某一峰值时就触发告警,也可以像卡顿链路回溯一样绑定上下文 timings数据来分析CPU异常归因。

在未来,这个功能还可能会被扩展到显示包括温度和电池状态等内容。

网络环境采集

为什么不能直接拿到网络环境数据呢?如果在 App 内, 我们可以通过 App 提供的接口获取到网络情况,但在端外(App 外部环境,比如微信里面的页面,或者PC站、手机浏览器下的页面)我们就没法直接拿到当前网络情况了。这时怎么办呢?

一个做法是拿到两张不同尺寸图片的加载时间,通过计算结果来判定当前网络环境。

具体来说,我们在每次页面加载时,通过客户端向服务端发送图片请求,比如,请求一张 11 像素的图片和一张 33 像素的图片,然后在图片请求之初打一个时间点,在图片 onLoad 完成后打一个时间点,两个时间点之差,就是图片的加载时间。

接着,我们用文件体积除以加载时间,就能得出两张图片的加载速度,然后把两张图片的加载速度求平均值,这个结果就可以当作网络速度了。

因为每个单页面启动时,都会做一次网速采集,得到一个网络速度,我们可以把这些网络速度做概率分布,就能得出当前网络情况是 2G (750-1400ms)、3G (230-750ms)、4G或者WiFi(0-230ms)。

下面这张图是我在做性能优化项目时,做的图片测速结果分布。横坐标是速度,纵坐标是网速在分布中的分位值,最左侧是 wifi网络,中间是 3G 网络,最右侧是 2G 网络。

根据这张图,你会发现自己的用户都停留在什么网段。比如,50% 的用户停留在 2G 水平。知道了这点,我们后续针对的优化手段就会更多侧重 2G 下的网络优化方案了。