在前端,监测用户在浏览界面过程中的卡顿(Jank)是优化用户体验的关键一环。卡顿通常表现为动画不流畅、滚动不平滑、点击无响应等。这通常是由于浏览器主线程被长时间占用,无法及时响应用户输入或更新渲染。
以下是多种监测卡顿的方法和代码示例,从底层 API 到高级工具:
1. 帧率监测 (FPS Monitoring)
帧率是衡量动画和滚动流畅度的最直观指标。通常,低于 60 FPS 就会开始感觉到卡顿。
原理:
利用 requestAnimationFrame
(rAF) 在浏览器下一次重绘前执行回调的特性。通过计算连续两次 rAF 回调之间的时间间隔,可以推算出当前的帧率。
代码示例:
js
/**
* 帧率监测器
* @param {function(number): void} onFPSChange - FPS 变化时的回调函数,参数为当前 FPS 值
* @param {number} interval - 报告 FPS 的间隔时间(毫秒),默认为 1000ms
*/
function setupFPSMonitor(onFPSChange, interval = 1000) {
let frameCount = 0;
let lastTime = performance.now();
let rafId;
function animate() {
const currentTime = performance.now();
frameCount++;
// 每隔一定时间报告一次 FPS
if (currentTime - lastTime >= interval) {
const fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
onFPSChange(fps);
frameCount = 0;
lastTime = currentTime;
}
rafId = requestAnimationFrame(animate);
}
// 启动监测
animate();
// 返回一个函数用于停止监测
return () => {
cancelAnimationFrame(rafId);
};
}
// 示例用法:
const stopFPSMonitor = setupFPSMonitor((fps) => {
console.log(`当前 FPS: ${fps}`);
// 如果 FPS 低于某个阈值,可以认为是卡顿
if (fps < 30) {
console.warn('检测到严重卡顿!当前 FPS:', fps);
// 可以在这里上报卡顿事件
}
});
// 模拟一个耗时操作来观察 FPS 下降
// setTimeout(() => {
// let sum = 0;
// for (let i = 0; i < 1000000000; i++) {
// sum += i;
// }
// console.log('耗时操作完成', sum);
// }, 2000);
// 停止监测 (可选)
// setTimeout(() => {
// stopFPSMonitor();
// console.log('FPS 监测已停止。');
// }, 10000);
优缺点:
- 优点: 简单易实现,直观反映 UI 流畅度。
- 缺点: 无法直接定位卡顿原因,只能知道"卡了",不知道"为什么卡"。高 FPS 并不意味着没有潜在的性能问题。
2. 长任务监测 (Long Task Monitoring)
长任务是指在浏览器主线程上运行时间超过 50 毫秒的任务。它们会阻塞主线程,导致页面无响应,是造成卡顿的主要原因。
原理:
使用 PerformanceObserver
监听 longtask
类型。当浏览器检测到长任务时,会触发回调。
代码示例:
js
/**
* 长任务监测器
* @param {function(PerformanceEntry): void} onLongTask - 检测到长任务时的回调函数
*/
function setupLongTaskMonitor(onLongTask) {
// 检查浏览器是否支持 Long Task API
if ('PerformanceObserver' in window && 'longtask' in PerformanceObserver.supportedEntryTypes) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.name: 通常是 "self"
// entry.duration: 任务持续时间 (毫秒)
// entry.startTime: 任务开始时间 (毫秒)
// entry.attribution: 任务归因信息 (例如,哪个脚本或元素导致)
console.warn('检测到长任务:', entry);
onLongTask(entry);
// 可以根据 duration 设置阈值进行上报
if (entry.duration > 100) { // 超过 100ms 的任务被认为是严重卡顿
console.error(`严重长任务:${entry.duration.toFixed(2)}ms`, entry);
// 可以在这里上报长任务事件,包含 entry 的详细信息
}
}
});
observer.observe({ entryTypes: ['longtask'] });
// 返回一个函数用于停止监测
return () => observer.disconnect();
} else {
console.warn('当前浏览器不支持 Long Task API。');
return () => {}; // 返回空函数
}
}
// 示例用法:
const stopLongTaskMonitor = setupLongTaskMonitor((task) => {
// console.log('长任务数据:', task);
// 在这里可以对长任务数据进行处理或上报
});
// 模拟一个长任务
function simulateLongTask() {
console.log('开始模拟长任务...');
let sum = 0;
for (let i = 0; i < 500000000; i++) { // 增加循环次数,确保超过 50ms
sum += i;
}
console.log('模拟长任务结束,结果:', sum);
}
// 在某个用户交互或定时器中触发长任务
// document.getElementById('myButton').addEventListener('click', simulateLongTask);
// 或者
setTimeout(simulateLongTask, 3000);
// 停止监测 (可选)
// setTimeout(() => {
// stopLongTaskMonitor();
// console.log('长任务监测已停止。');
// }, 10000);
优缺点:
- 优点: 直接定位到导致卡顿的"罪魁祸首"(长任务),提供任务的持续时间和归因信息,有助于调试。
- 缺点: 并非所有浏览器都完全支持
attribution
属性,且无法捕获所有类型的卡顿(例如,GPU 渲染瓶颈)。
3. 交互延迟监测 (Interaction Latency Monitoring)
这衡量的是用户从输入(如点击、按键)到浏览器开始处理该输入之间的时间。Web Vitals 中的 FID (First Input Delay) 就是一个重要的指标。
原理:
使用 PerformanceObserver
监听 event
类型,特别是 first-input
。
代码示例:
js
/**
* 交互延迟监测器 (FID)
* @param {function(number): void} onFIDChange - FID 变化时的回调函数,参数为 FID 值(毫秒)
*/
function setupFIDMonitor(onFIDChange) {
if ('PerformanceObserver' in window && 'event' in PerformanceObserver.supportedEntryTypes) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'first-input') {
// FID = start time of processing - start time of input
const fid = entry.processingStart - entry.startTime;
console.log(`首次输入延迟 (FID): ${fid.toFixed(2)}ms`, entry);
onFIDChange(fid);
if (fid > 100) { // 超过 100ms 的 FID 体验不佳
console.warn(`首次输入延迟过高:${fid.toFixed(2)}ms`, entry);
// 可以在这里上报 FID 事件
}
// 首次输入延迟通常只计算一次,所以可以断开观察
observer.disconnect();
}
}
});
observer.observe({ type: 'event', buffered: true }); // buffered: true 获取页面加载前的事件
return () => observer.disconnect();
} else {
console.warn('当前浏览器不支持 Event Timing API 或 First Input.');
return () => {};
}
}
// 示例用法:
const stopFIDMonitor = setupFIDMonitor((fid) => {
// console.log('FID 数据:', fid);
// 在这里可以对 FID 数据进行处理或上报
});
// 模拟一个阻塞主线程的场景,来观察 FID
document.addEventListener('DOMContentLoaded', () => {
const button = document.createElement('button');
button.textContent = '点击我(可能延迟)';
document.body.appendChild(button);
button.addEventListener('click', () => {
console.log('按钮被点击了!');
// 模拟一个长任务,来观察点击后的延迟
let sum = 0;
for (let i = 0; i < 200000000; i++) {
sum += i;
}
console.log('点击处理完成', sum);
});
});
// 停止监测 (可选)
// setTimeout(() => {
// stopFIDMonitor();
// console.log('FID 监测已停止。');
// }, 15000);
优缺点:
- 优点: 直接反映用户感知的交互响应速度,是衡量用户体验的关键指标之一。
- 缺点: 只测量首次输入延迟,后续交互的延迟需要其他方法(如 INP)。
4. 布局偏移监测 (Layout Shift Monitoring)
布局偏移是指页面元素在用户不知情的情况下发生移动,导致用户误操作或视觉混乱。虽然不是直接的"卡顿",但严重影响用户体验。Web Vitals 中的 CLS (Cumulative Layout Shift) 就是衡量这个指标。
原理:
使用 PerformanceObserver
监听 layout-shift
类型。
代码示例:
js
/**
* 布局偏移监测器 (CLS)
* @param {function(number): void} onCLSChange - CLS 变化时的回调函数,参数为当前累积 CLS 值
*/
function setupCLSMonitor(onCLSChange) {
if ('PerformanceObserver' in window && 'layout-shift' in PerformanceObserver.supportedEntryTypes) {
let cls = 0;
let sessionWindow = []; // 用于计算会话窗口内的 CLS
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 排除输入导致的布局偏移 (例如,键盘输入或点击导致的元素移动)
if (entry.hadRecentInput) {
continue;
}
const currentTime = entry.startTime;
const currentShiftTime = entry.startTime;
// 移除会话窗口中过期的偏移 (在 1 秒内,或与上一个偏移间隔超过 500ms)
sessionWindow = sessionWindow.filter(shift => {
return currentTime - shift.lastShiftTime < 1000 && currentTime - shift.startTime < 500;
});
// 将当前偏移添加到会话窗口
sessionWindow.push({
startTime: entry.startTime,
lastShiftTime: entry.startTime,
value: entry.value
});
// 计算当前会话窗口的 CLS
let sessionValue = 0;
for (const shift of sessionWindow) {
sessionValue += shift.value;
}
// 更新总 CLS (通常是累积的)
cls += entry.value;
console.log(`检测到布局偏移: ${entry.value.toFixed(4)}`, entry);
console.log(`当前累积 CLS: ${cls.toFixed(4)}`);
onCLSChange(cls); // 报告累积 CLS
}
});
observer.observe({ entryTypes: ['layout-shift'] });
return () => observer.disconnect();
} else {
console.warn('当前浏览器不支持 Layout Shift API。');
return () => {};
}
}
// 示例用法:
const stopCLSMonitor = setupCLSMonitor((currentCLS) => {
// console.log('当前 CLS:', currentCLS);
if (currentCLS > 0.1) { // CLS 超过 0.1 体验不佳
console.error(`累积布局偏移过高:${currentCLS.toFixed(4)}`);
// 可以在这里上报 CLS 事件
}
});
// 模拟一个布局偏移
document.addEventListener('DOMContentLoaded', () => {
const container = document.createElement('div');
container.style.border = '1px solid #ccc';
container.style.padding = '10px';
container.style.marginBottom = '20px';
container.textContent = '这是一个容器。';
document.body.appendChild(container);
setTimeout(() => {
// 插入一个图片,导致下方内容下移
const img = document.createElement('img');
img.src = 'https://via.placeholder.com/150'; // 替换为实际图片 URL
img.alt = 'Placeholder Image';
img.style.display = 'block';
img.style.marginBottom = '10px';
container.prepend(img); // 在容器头部插入图片
console.log('插入图片,可能导致布局偏移。');
}, 2000);
setTimeout(() => {
// 动态改变元素高度
container.style.height = '200px';
console.log('改变容器高度,可能导致布局偏移。');
}, 4000);
});
// 停止监测 (可选)
// setTimeout(() => {
// stopCLSMonitor();
// console.log('CLS 监测已停止。');
// }, 15000);
优缺点:
- 优点: 直接反映页面视觉稳定性,有助于发现因动态内容加载、字体加载等导致的页面跳动问题。
- 缺点: 并非直接的"卡顿",但与用户体验紧密相关。
5. 综合使用 Web Vitals 指标 (LCP, FID, CLS, INP)
Google 的 Web Vitals 是一组核心指标,旨在量化用户体验。它们是上述各种监测方法的综合体现。
- LCP (Largest Contentful Paint): 最大内容绘制,衡量页面的加载性能。
- FID (First Input Delay): 首次输入延迟,衡量页面交互性(已在上面介绍)。
- CLS (Cumulative Layout Shift): 累积布局偏移,衡量页面视觉稳定性(已在上面介绍)。
- INP (Interaction to Next Paint): 交互到下一次绘制,衡量页面对用户交互的整体响应能力。这是 FID 的升级版,它会测量所有交互(而不仅仅是第一次),并关注交互处理到下一次视觉更新之间的时间。
如何获取:
Google 提供了 web-vitals
库,可以方便地获取这些指标。
代码示例 (使用 web-vitals
库):
首先,安装 web-vitals
:
npm install web-vitals
或 yarn add web-vitals
然后,在你的代码中:
js
// web-vitals.js (或你的入口文件)
import { onCLS, onFID, onLCP, onINP } from 'web-vitals';
function sendToAnalytics(metric) {
console.log(`Web Vitals Metric: ${metric.name} - ${metric.value.toFixed(2)}`, metric);
// 在这里可以将数据发送到你的后端分析服务
// 例如:fetch('/api/metrics', { method: 'POST', body: JSON.stringify(metric) });
// 可以根据不同的指标类型和值进行预警
if (metric.name === 'FID' && metric.value > 100) {
console.error('FID 警告:', metric.value);
} else if (metric.name === 'CLS' && metric.value > 0.1) {
console.error('CLS 警告:', metric.value);
} else if (metric.name === 'LCP' && metric.value > 2500) {
console.error('LCP 警告:', metric.value);
} else if (metric.name === 'INP' && metric.value > 200) {
console.error('INP 警告:', metric.value);
}
}
// 注册所有核心 Web Vitals 指标的监听
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics); // INP 仍在实验阶段,但非常重要
console.log('Web Vitals 监测已启动。');
优缺点:
- 优点: 官方推荐,全面覆盖用户体验的关键方面,易于集成和使用,数据标准化。
- 缺点: 依赖第三方库,INP 等指标可能仍在演进中。
6. 浏览器开发者工具 (Browser DevTools)
虽然不是代码层面的实时监测,但浏览器自带的开发者工具是调试和分析卡顿最强大的工具。
- Performance (性能) 面板: 记录页面活动,可视化主线程、GPU、网络等活动,可以清晰地看到长任务、布局、重绘等耗时操作。
- Lighthouse: 自动化审计工具,提供性能、可访问性、最佳实践等方面的报告,会给出 Web Vitals 的分数和改进建议。
- Memory (内存) 面板: 检查内存泄漏,内存泄漏也可能导致卡顿。
使用方法:
- 打开开发者工具 (F12 或右键检查)。
- 切换到
Performance
或Lighthouse
面板。 - 点击录制按钮或运行审计。
- 分析报告和时间轴。
优缺点:
- 优点: 功能强大,提供详细的性能数据和可视化,是定位问题根源的利器。
- 缺点: 只能在开发环境或用户主动操作下使用,无法进行大规模的用户行为实时监测。
7. 第三方性能监控工具 (Real User Monitoring - RUM)
专业的 APM (Application Performance Management) 工具,如 Sentry, Datadog, New Relic, Fundebug, OneAPM 等,都提供了前端性能监控功能。
原理:
这些工具通常会在你的页面中嵌入一个 SDK,该 SDK 会自动收集上述各种性能指标(包括 Web Vitals、自定义事件耗时、资源加载时间等),并将数据上报到其云平台进行聚合、分析和可视化。
代码示例 (以 Sentry 为例,其他类似):
js
// main.js 或你的初始化文件
import * as Sentry from '@sentry/browser';
import { Integrations } from '@sentry/tracing'; // 如果需要追踪性能
Sentry.init({
dsn: "YOUR_SENTRY_DSN", // 替换为你的 Sentry DSN
integrations: [
new Integrations.BrowserTracing(), // 启用性能追踪
],
// 采样率,例如 1.0 表示捕获所有事务,0.1 表示捕获 10%
tracesSampleRate: 1.0,
// 也可以自定义性能指标
// hooks: {
// beforeSendTransaction: (transaction) => {
// // 可以在这里添加自定义标签或数据
// return transaction;
// }
// }
});
// 手动追踪一个自定义操作的性能
const transaction = Sentry.startTransaction({ name: "Custom Operation" });
const span = transaction.startChild({ op: "function_call", description: "Heavy calculation" });
// 模拟一个耗时操作
setTimeout(() => {
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += i;
}
console.log('自定义操作完成', sum);
span.finish();
transaction.finish();
}, 500);
// Sentry 还会自动捕获长任务、资源加载、路由切换等性能数据
优缺点:
- 优点: 开箱即用,提供全面的性能数据、错误监控、告警系统,能聚合大量真实用户数据,便于发现共性问题。
- 缺点: 通常需要付费,数据隐私和安全性考量,可能增加页面加载负担(尽管通常很小)。
总结与建议
-
优先级:
- Web Vitals (尤其是 FID 和 INP): 这是衡量用户体验最权威和全面的指标,应优先关注。使用
web-vitals
库是最推荐的方式。 - 长任务监测: 直接定位卡顿的根本原因,非常有助于调试。
- FPS 监测: 作为辅助,直观反映流畅度,但不能提供具体原因。
- 布局偏移监测: 关注视觉稳定性,避免意外的页面跳动。
- Web Vitals (尤其是 FID 和 INP): 这是衡量用户体验最权威和全面的指标,应优先关注。使用
-
数据上报: 无论是哪种监测方式,最终都需要将数据上报到后端服务进行存储、分析和可视化。这才能形成完整的 RUM (Real User Monitoring) 体系。
-
结合调试: 监测发现问题后,利用浏览器开发者工具(Performance 面板)进行深入分析和定位,找出具体的代码瓶颈。
-
持续优化: 性能优化是一个持续的过程,需要不断监测、分析、改进。
通过结合使用上述方法,你可以全面、准确地监测前端界面的卡顿情况,从而不断提升用户体验。