监测用户在浏览界面过程中的卡顿

在前端,监测用户在浏览界面过程中的卡顿(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-vitalsyarn 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 (内存) 面板: 检查内存泄漏,内存泄漏也可能导致卡顿。

使用方法:

  1. 打开开发者工具 (F12 或右键检查)。
  2. 切换到 PerformanceLighthouse 面板。
  3. 点击录制按钮或运行审计。
  4. 分析报告和时间轴。

优缺点:

  • 优点: 功能强大,提供详细的性能数据和可视化,是定位问题根源的利器。
  • 缺点: 只能在开发环境或用户主动操作下使用,无法进行大规模的用户行为实时监测。

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 还会自动捕获长任务、资源加载、路由切换等性能数据

优缺点:

  • 优点: 开箱即用,提供全面的性能数据、错误监控、告警系统,能聚合大量真实用户数据,便于发现共性问题。
  • 缺点: 通常需要付费,数据隐私和安全性考量,可能增加页面加载负担(尽管通常很小)。

总结与建议

  1. 优先级:

    • Web Vitals (尤其是 FID 和 INP): 这是衡量用户体验最权威和全面的指标,应优先关注。使用 web-vitals 库是最推荐的方式。
    • 长任务监测: 直接定位卡顿的根本原因,非常有助于调试。
    • FPS 监测: 作为辅助,直观反映流畅度,但不能提供具体原因。
    • 布局偏移监测: 关注视觉稳定性,避免意外的页面跳动。
  2. 数据上报: 无论是哪种监测方式,最终都需要将数据上报到后端服务进行存储、分析和可视化。这才能形成完整的 RUM (Real User Monitoring) 体系。

  3. 结合调试: 监测发现问题后,利用浏览器开发者工具(Performance 面板)进行深入分析和定位,找出具体的代码瓶颈。

  4. 持续优化: 性能优化是一个持续的过程,需要不断监测、分析、改进。

通过结合使用上述方法,你可以全面、准确地监测前端界面的卡顿情况,从而不断提升用户体验。

相关推荐
拾光拾趣录1 小时前
基础 | HTML语义、CSS3新特性、浏览器存储、this、防抖节流、重绘回流、date排序、calc
前端·面试
小小小小宇2 小时前
前端监测用户卡顿之INP
前端
糖墨夕2 小时前
Nest 是隐藏的“设计模式大佬”
前端
逾明3 小时前
Electron自定义菜单栏及Mac最大化无效的问题解决
前端·electron
辰九九3 小时前
Uncaught URIError: URI malformed 报错如何解决?
前端·javascript·浏览器
月亮慢慢圆3 小时前
Echarts的基本使用(待更新)
前端
芜青3 小时前
实现文字在块元素中水平/垂直居中详解
前端·css·css3
useCallback3 小时前
Elpis全栈项目总结
前端
小高0074 小时前
React useMemo 深度指南:原理、误区、实战与 2025 最佳实践
前端·javascript·react.js