Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何监控页面是否卡顿 #34

Open
liangbus opened this issue Apr 2, 2020 · 3 comments
Open

如何监控页面是否卡顿 #34

liangbus opened this issue Apr 2, 2020 · 3 comments

Comments

@liangbus
Copy link
Owner

liangbus commented Apr 2, 2020

前言

曾经面试的时候被问到过:如果有客户反馈你的页面在他那边出现卡顿,你应该怎么去定位问题。
当时脑子抽风了,没做过这方面的,只好说性能优化什么的,发发埋点。。。遂卒

最近突然又想起这个问题,查阅了一些资料,然后就有了这篇东西

理论背景

首先,卡顿是我们视觉上的直观感觉,那转换成技术上的术语,那就是掉帧,这时候就得说说 FPS (Frames Per Second) 这个概念,这个在视频或者游戏方面有了解比较多的同学肯定不会陌生,它就是描述画面每秒钟传输的帧数,帧数越高,视觉上就感觉越流畅

一般来说对网页来说,60 fps 为最优,30 ~ 60也是可以接受的,30 以下就需要考虑下优化
在 Chrome 下通过开发者工具,Performance 页卡可以查看页面的 FPS

image

JS 是单线程执行的,并且和 GUI 渲染线程互斥,如果 FPS 过低,一般来说是 JS 执行任务长时间占据了线程导致页面页面停止渲染,从而造成卡顿的视觉效果

想人为测试页面卡顿的话,弄个类似 sleep 的函数即可
示例

function sleep(t) {
  const ts = Date.now()
  let now = ts
  while(now < ts + t) {
    now = Date.now()
  }
}

有了上面的基础知识,那回到正题,怎么检测页面的卡顿?
当然就是检测用户的 FPS 啦,这时就得介绍一下 requestanimationframe

requestanimationframe 会根据屏幕刷新的频率来执行该函数,入参是一个回调

对兼容性有要求的,可以使用 polyfill for requestanimationframe

if (!Date.now)
    Date.now = function() { return new Date().getTime(); };

(function() {
    'use strict';
    
    var vendors = ['webkit', 'moz'];
    for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
        var vp = vendors[i];
        window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
        window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']
                                   || window[vp+'CancelRequestAnimationFrame']);
    }
    if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy
        || !window.requestAnimationFrame || !window.cancelAnimationFrame) {
        var lastTime = 0;
        window.requestAnimationFrame = function(callback) {
            var now = Date.now();
            var nextTime = Math.max(lastTime + 16, now);
            return setTimeout(function() { callback(lastTime = nextTime); },
                              nextTime - now);
        };
        window.cancelAnimationFrame = clearTimeout;
    }
}());

检测思路

requestAnimationFrame 接受一个回调函数,浏览器每次重绘都执行这个回调函数,那我们就写一个回调函数就好了。

BTW,requestAnimationFrame 是异步调用的,且为宏事件(Macro Task)哦

由于浏览器每帧刷新都会调用该回调,那我们可以记录某个时间段帧刷新的次数,然后除以该时间,那就可以得出当前时间段的帧率啦(一般设置1秒比较好计算和理解),把 FPS 存起来,如果连续多次 FPS 低于某个设定的临界值,我们就上报数据,核心内容就是这么简单~

另外要注意的是,由于该函数每秒约被调用60次,属于比较高频的了,不能够为了检测性能问题去引入性能问题,而且我们也没必要时刻去检测,所以可以通过设定一个定时器,在一定时间内只检测一次,这样就可以避免造成额外的负担

下面直接上检测页面卡顿与否的核心代码

const framesMonitor = (() => {
  // const SIXTY_TIMES = 60;
  const requestAnimationFrame = window.requestAnimationFrame;
  if (requestAnimationFrame) {
    return (cb) => {
      const timer = requestAnimationFrame(() => {
        cb();
        window.cancelAnimationFrame(timer);
      });
    };
  // requestAnimationFrame 兼容实现
  }
})();
const ONE_SECOND = 1000

function stuck() {
  const stucksFPS = [];
  const startTime = Date.now();
  const loop = (startCountTime = Date.now(), lastFrameCount = 0) => {
    const now = Date.now();
    // 每一帧进来,计数+1,传参累计
    const accFrameCount = lastFrameCount + 1;
    // console.log('accFrameCount', accFrameCount)
    // 大于等于一秒钟为一个周期;比如如果是正常的fps: 那当第61次时,按最优1秒60帧,即(1/60)*61 = 1017毫秒,这里就满足
    if (now > ONE_SECOND + startCountTime) {
      // 计算经过的时间间隔值,换算成秒
      const timeInterval = (now - startCountTime) / ONE_SECOND;
      // 计算一秒钟的fps: 当前计数总次数 / 经过的时长;
      const fps = Math.round(accFrameCount / timeInterval);
      if (fps > 30) { // fps 小于30 判断为卡顿
        stucksFPS.pop();
      } else {
        stucksFPS.push(fps);
      }
      // 连续三次小于30 上报卡顿(还有一种特殊情况,前面2次卡顿,第三次不卡,接着再连续两次卡顿,也满足)
      if (stucksFPS.length === 3) {
          console.log(new Error(`Page Stuck captured: ${location.href} ${stucksFPS.join(',')} ${now - startTime}ms`));
        // 清空采集到的卡顿数据
        stucksFPS.length = 0;
      }
      // 避免持续采集,休息一个周期(这里定义的是一分钟),重新开启采样
      const timer = setTimeout(() => {
        loop();
        clearTimeout(timer);
      }, 60 * 1000);
      return;
    }
    framesMonitor(() => loop(startCountTime, accFrameCount));
  };
  loop();
};

参考:
如何检测页面卡顿
前端监控体系怎么搭建
Polyfill for requestAnimationFrame/cancelAnimationFrame

@Thulof
Copy link

Thulof commented Jan 19, 2022

有一个问题,如果用户切换 Tab,那么 rAF 就会暂停,这样就会很影响数据

@liangbus
Copy link
Owner Author

有一个问题,如果用户切换 Tab,那么 rAF 就会暂停,这样就会很影响数据

@Thulof 切换 tab 是指像 app 底部那种 tabs ?那应该增加个侦听跳出事件,来取消 raf 吧

@JackieZhang-Coding
Copy link

请问检测到页面卡顿之后需要怎么优化?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants