一、前言
页面卡顿是用户在使用过程中经常遇到的一种情况,它表现为页面响应速度明显变慢,用户的操作难以获得即时反馈,例如,用户点击按钮后需等待许久才见反应,或在滚动页面时频繁遭遇卡顿,这些情况都会显著降低用户的操作效率和满意度。尽管页面卡顿的影响程度较页面崩溃、白屏和报错等情形稍轻,但若频繁发生,也会给用户带来极大困扰,严重影响其使用体验。随着前端技术的持续发展,应用的复杂度不断攀升,页面卡顿的成因愈发复杂多样,使得卡顿问题变得难以捉摸和精准定位。这无疑给前端监控页面卡顿带来了巨大挑战,但同时也凸显了其重要性与价值。本文将深入探讨前端监控页面卡顿的有效方法与策略,力图为前端开发者提供一套全面且实用的解决方案,助力攻克页面卡顿难题,提升用户体验。
二、卡顿的定义与表现
前端卡顿通常指的是页面在渲染、交互或动画过程中,由于性能瓶颈而导致的界面响应迟滞的现象。具体表现为:
- 页面渲染延迟:用户操作后,页面未能及时响应。
- 动画不流畅:过渡或动画效果出现停顿或卡顿现象。
- 输入延迟:用户输入(如键盘输入、鼠标点击)后,界面反应滞后。
- 滚动卡顿:页面滚动过程中出现画面拖影或不平滑。
- ......
卡顿通常由浏览器主线程被阻塞引起,而主线程的任务主要包括 DOM 渲染、JavaScript 执行、布局计算和绘制等。每当主线程任务过多,尤其是 JavaScript 执行时间过长时,卡顿现象就容易出现。
三、卡顿监测方案
3.1 帧率检测
帧率(FPS)是衡量页面卡顿程度的指标之一,它代表着浏览器重新计算、布局以及将内容绘制到显示器上的速率,也就是每秒钟能够完成的重新绘制帧数。一般来说,为了确保页面的流畅运行和快速响应,页面的帧率应维持在 60FPS 或更高水平,这样才能让用户在浏览和交互过程中感受到丝滑无卡顿的体验。帧率的计算我们可以通过 requestAnimationFrame API 来实现,其会在浏览器下次重绘之前调用。卡顿的判断主要有以下几种规则:
- 判断两帧的时间差是否超过一定的阈值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);
- 判断帧率是否低于一定的阈值 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”选项,即可直观查看到帧率情况。
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 监听等,可以有效地监测页面卡顿问题。在实际应用中,可以根据具体需求选择合适的方案,提升用户体验。卡顿作为性能问题的表现之一,推荐结合《性能优化进阶 💥 前端性能埋点&监控体系建设!》一起阅读。
参考资料
「前端监控专栏」更多内容 👇