实时洞察 🔍 前端页面卡顿监测技术方案

4,025 阅读7分钟

掘金横图.png

一、前言

页面卡顿是用户在使用过程中经常遇到的一种情况,它表现为页面响应速度明显变慢,用户的操作难以获得即时反馈,例如,用户点击按钮后需等待许久才见反应,或在滚动页面时频繁遭遇卡顿,这些情况都会显著降低用户的操作效率和满意度。尽管页面卡顿的影响程度较页面崩溃、白屏和报错等情形稍轻,但若频繁发生,也会给用户带来极大困扰,严重影响其使用体验。随着前端技术的持续发展,应用的复杂度不断攀升,页面卡顿的成因愈发复杂多样,使得卡顿问题变得难以捉摸和精准定位。这无疑给前端监控页面卡顿带来了巨大挑战,但同时也凸显了其重要性与价值。本文将深入探讨前端监控页面卡顿的有效方法与策略,力图为前端开发者提供一套全面且实用的解决方案,助力攻克页面卡顿难题,提升用户体验。

二、卡顿的定义与表现

前端卡顿通常指的是页面在渲染、交互或动画过程中,由于性能瓶颈而导致的界面响应迟滞的现象。具体表现为:

  • 页面渲染延迟:用户操作后,页面未能及时响应。
  • 动画不流畅:过渡或动画效果出现停顿或卡顿现象。
  • 输入延迟:用户输入(如键盘输入、鼠标点击)后,界面反应滞后。
  • 滚动卡顿:页面滚动过程中出现画面拖影或不平滑。
  • ......

卡顿通常由浏览器主线程被阻塞引起,而主线程的任务主要包括 DOM 渲染、JavaScript 执行、布局计算和绘制等。每当主线程任务过多,尤其是 JavaScript 执行时间过长时,卡顿现象就容易出现。

三、卡顿监测方案

3.1 帧率检测

帧率(FPS)是衡量页面卡顿程度的指标之一,它代表着浏览器重新计算、布局以及将内容绘制到显示器上的速率,也就是每秒钟能够完成的重新绘制帧数。一般来说,为了确保页面的流畅运行和快速响应,页面的帧率应维持在 60FPS 或更高水平,这样才能让用户在浏览和交互过程中感受到丝滑无卡顿的体验。帧率的计算我们可以通过 requestAnimationFrame API 来实现,其会在浏览器下次重绘之前调用。卡顿的判断主要有以下几种规则:

  1. 判断两帧的时间差是否超过一定的阈值X,当超过阈值 X 或超过阈值 n 次则判定卡顿;
const FPS = 60; // 帧率
const FRAME_TIME = 1000 / FPS; // 每帧所需的时间
const CHECK_COUNT = 2; // 检测次数

let unmetCount = 0; // 不满足阈值的次数
let lastFrameTime = 0; // 上一帧的时间戳

const checkFrame = (currentFrameTime) => {
  if (currentFrameTime - lastFrameTime >= 1000) {
    console.log(currentFrameTime, lastFrameTime, currentFrameTime - lastFrameTime, FRAME_TIME);
    unmetCount++;
    if (unmetCount >= CHECK_COUNT) {
      console.warn("页面出现卡顿现象!");
    }
  } else {
    unmetCount = 0;
  }
  lastFrameTime = currentFrameTime;
  requestAnimationFrame(checkFrame);
};

requestAnimationFrame(checkFrame);
  1. 判断帧率是否低于一定的阈值 X,当帧率低于阈值 X 或连续 n 次低于阈值则判定卡顿;
let prevTimestamp = 0; // 记录上一次的时间戳
let count = 0; // 记录帧数

const showFPS = (fps) => {
  console.log(fps);
};

const getFPS = (currentTime) => {
  if (prevTimestamp) {
    count++;
    if (currentTime - prevTimestamp >= 1000) {
      showFPS(count);
      count = 0;
      prevTimestamp = currentTime;
    }
  } else {
    prevTimestamp = currentTime;
  }
  requestAnimationFrame(getFPS);
};

requestAnimationFrame(getFPS);

帧率检测是一种较为粗略的估算过程,且需要持续不断地进行计算,这往往会带来大量不必要的开销。对于性能要求较高的系统而言,采用这种方法来检测卡顿很可能会对其运行产生一定的负面影响。若想查看浏览器的真实帧率,可打开控制台,按下 Command + Shift + P 键,然后选择“Show Frames Per Second (FPS) Meter”选项,即可直观查看到帧率情况。

截屏2025-01-19 14.00.33.png

3.2 心跳检测

心跳检测可以通过定时器(setInterval 或 setTimeout)来定期检测页面是否响应,如果页面在预定时间内没有响应,就可以判断为卡顿。但是,页面卡顿时,通常是由于主线程在执行复杂的计算任务所导致。由于 JavaScript 本质上是单线程的,大量的计算任务会阻塞主线程,使其无法及时响应其他操作。鉴于此,我们还可以跳出主线程来进行卡顿检测,利用 Worker 来实现这一目的。Worker 能够在独立的线程中执行 JavaScript 代码,即便主线程处于忙碌状态,Worker 所在的线程也不会受到影响。凭借这一特性,我们可以在页面加载时启动一个 Worker 线程,并设置主线程每隔一定时间(例如 1 秒)向 Worker 发送一次心跳消息。若页面发生卡顿,主线程因忙碌而无法按时发送心跳消息,Worker 在检测到心跳丢失时便进行上报,从而能够及时发现卡顿的发生,以便采取相应的优化措施。线程通信本身需要一些耗时,且 JavaScript 的计时器未必是准时的,因此心跳检测是一种近似检测方式。Worker 可以使用 Web Worker 或者 Service Worker,这里我们以 Web Worker 为例。

// worker.js
const heartbeatInterval = 500; // 心跳间隔
const maxLagThreshold = 1000; // 最大容忍卡顿时间,超过1秒视为卡顿
let lastTimestamp = Date.now();
let isLagging = false; // 是否处于卡顿状态

self.onmessage = function (e) {
  // 接收到主线程的时间戳
  lastTimestamp = e.data.timestamp;
};

function heartbeatCheck() {
  const currentTimestamp = Date.now();
  const timeDifference = currentTimestamp - lastTimestamp;

  // 如果时间差超过最大容忍的阈值,认为主线程卡顿
  if (timeDifference > maxLagThreshold) {
    if (!isLagging) {
      isLagging = true;
      console.log({ type: "lag", message: `警告:主线程卡顿!上次响应与当前响应时间差为:${timeDifference}ms` });
    }
  } else {
    if (isLagging) {
      isLagging = false;
      console.log({ type: "normal", message: "主线程恢复正常响应" });
    }
  }
}

setInterval(heartbeatCheck, heartbeatInterval);
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      const worker = new Worker("worker.js");

      // 定时向 Worker 发送时间戳
      setInterval(() => {
        lastTimestamp = Date.now();
        worker.postMessage({ timestamp: lastTimestamp });
      }, 500); // 每500ms发送一次时间戳

      document.getElementById("button").addEventListener("click", function () {
        // 模拟页面卡顿
        for (let i = 0; i < 1000000000; i++) {
          // do nothing
        }
      });
    </script>
  </body>
</html>

3.3 Long Tasks 监听

当任务阻塞主线程达到 50 毫秒或更长时间时,将引发诸多问题,例如,可交互时间延迟、严重不稳定的交互行为 (轻击、单击、滚动、滚轮等) 延迟、严重不稳定的事件回调延迟、紊乱的动画和滚动等。我们将任何主 UI 线程连续不间断繁忙 5 0毫秒及以上的时间区间定义为长任务(Long task)。目前,浏览器中存在一个 Long Tasks API,借助该 API 与PerformanceObserver 的结合使用,我们能够精准地定位长任务,从而有效实现对卡顿现象的检测。当然,当前这还不是一个标准,因此存在一定的兼容性问题。

var observer = new PerformanceObserver(function (list) {
  var perfEntries = list.getEntries();
  for (var i = 0; i < perfEntries.length; i++) {
    // Process long task notifications:
    // report back for analytics and monitoring
    // ...
  }
});
// register observer for long task notifications
observer.observe({ entryTypes: ["longtask"] });
// Long script execution after this will result in queueing
// and receiving "longtask" entries in the observer.

3.4 其他方法

此外,还可以通过监测具体操作的渲染延迟来判断卡顿。例如,使用 requestIdleCallback 来检测浏览器的空闲时间是否充足;或者利用 performance.mark 和 performance.measure 来精确测量渲染和用户交互的延迟;或者使用 PerformanceObserver 可以监听各种性能事件,包括长任务、资源加载、导航事件等,通过监听这些事件,可以检测页面的卡顿情况。

四、总结

卡顿监测是前端性能优化的重要环节,通过使用帧率检测、Worker 心跳方案、 Long Tasks API,并结合 requestAnimationFrame 方案和 PerformanceObserver 监听等,可以有效地监测页面卡顿问题。在实际应用中,可以根据具体需求选择合适的方案,提升用户体验。卡顿作为性能问题的表现之一,推荐结合《性能优化进阶 💥 前端性能埋点&监控体系建设!》一起阅读。

参考资料

「前端监控专栏」更多内容 👇