前端PerformanceObserver

PerformanceObserver 是 Web Performance API 的一部分,它提供了一种异步、高效地收集浏览器性能指标的方式。与传统的 performance.getEntries()performance.getEntriesByType() 方法不同,PerformanceObserver 允许你订阅特定类型的性能事件,并在这些事件发生时获得通知,而不是在某个时间点一次性获取所有已发生的事件。这对于实时监控和报告性能数据非常有用,尤其是在单页应用 (SPA) 中,因为页面加载后的用户交互也会产生新的性能事件。

PerformanceObserver 的作用

PerformanceObserver 主要用于:

  1. 监控页面加载性能: 获取导航时间、资源加载时间等。
  2. 跟踪用户体验指标: 例如 First Contentful Paint (FCP)、Largest Contentful Paint (LCP)、Cumulative Layout Shift (CLS) 等 Core Web Vitals 指标。
  3. 识别性能瓶颈: 发现长时间运行的任务 (Long Tasks) 或渲染阻塞。
  4. 实时数据收集: 异步获取性能数据,方便发送到分析服务。

PerformanceObserver 可以观察的性能条目类型 (Entry Types)

PerformanceObserver 可以观察多种性能条目类型,这些类型通过 observe() 方法的 entryTypes 选项指定:

  • 'navigation' : 测量主文档的导航和加载时间。
  • 'resource' : 测量页面上所有资源的加载时间(如图片、CSS、JavaScript 文件、XHR 请求等)。
  • 'paint' : 测量渲染事件,主要用于 First Contentful Paint (FCP) 和 Largest Contentful Paint (LCP)。
  • 'longtask' : 识别主线程上耗时超过 50 毫秒的任务。
  • 'layout-shift' : 测量页面布局的意外移动,用于计算 Cumulative Layout Shift (CLS)。
  • 'element' : 允许你观察特定 DOM 元素的渲染时间,通常用于更精确的 LCP 测量。
  • 'event' : 测量用户事件(如点击、键盘输入)的延迟。
  • 'mark''measure' : 通过 performance.mark()performance.measure() 自定义性能标记和测量。

PerformanceObserver 的基本用法

PerformanceObserver 的构造函数接收一个回调函数,当观察到的性能事件发生时,这个回调函数会被调用。回调函数会接收一个 PerformanceObserverEntryList 对象和一个 PerformanceObserver 实例作为参数。

js 复制代码
const observer = new PerformanceObserver((entryList, observer) => {
  // 当观察到的性能事件发生时,这个回调函数会被调用
  // entryList 包含了所有新发生的性能条目
  // observer 是当前的 PerformanceObserver 实例
});

observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] });
// 开始观察指定类型的性能事件

// 在不再需要观察时调用 disconnect()
// observer.disconnect();

详细代码讲解与示例

示例 1: 监控导航时间 (Navigation Timing)

导航时间提供了页面从开始加载到完全加载的各个阶段的时间点。

js 复制代码
// 1. 创建 PerformanceObserver 实例
const navObserver = new PerformanceObserver((entryList) => {
  // 2. 回调函数被调用时,获取所有新发生的性能条目
  const entries = entryList.getEntriesByType('navigation');

  // 遍历这些条目,通常 'navigation' 类型只有一个条目
  entries.forEach((entry) => {
    console.log('--- 导航性能数据 ---');
    console.log('页面加载时间:', entry.duration.toFixed(2), 'ms'); // 从 fetchStart 到 loadEventEnd
    console.log('DOM 解析完成:', entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart, 'ms');
    console.log('资源加载完成:', entry.loadEventEnd - entry.loadEventStart, 'ms');
    console.log('首次请求字节:', entry.responseStart - entry.requestStart, 'ms');
    console.log('完整导航条目:', entry);

    // 可以在这里将数据发送到后端分析服务
    // sendToAnalyticsService({
    //   type: 'navigation',
    //   duration: entry.duration,
    //   // ... 其他相关指标
    // });
  });

  // 导航事件通常只发生一次,所以一旦获取到数据就可以断开观察
  navObserver.disconnect();
});

// 3. 开始观察 'navigation' 类型的性能事件
// { buffered: true } 选项表示在 observer 创建之前发生的匹配事件也会被包含在第一次回调中
navObserver.observe({ entryTypes: ['navigation'], buffered: true });

console.log('PerformanceObserver for navigation timing started.');

解释:

  • entry.duration: 表示从 fetchStartloadEventEnd 的总时间,通常被认为是页面加载的总耗时。
  • buffered: true: 这个选项非常重要。对于某些只发生一次或在 PerformanceObserver 实例化之前就可能发生的事件(如 navigationpaint),buffered: true 会让观察者在第一次回调时也包含这些"历史"事件。

示例 2: 监控资源加载时间 (Resource Timing)

资源时间可以帮助你了解页面上每个资源(图片、脚本、样式表、字体、XHR/Fetch 请求等)的加载性能。

js 复制代码
const resourceObserver = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntriesByType('resource');

  entries.forEach((entry) => {
    // 过滤掉不重要的资源或非 HTTP/HTTPS 协议的资源
    if (entry.name.startsWith('http') || entry.name.startsWith('https')) {
      console.log('--- 资源加载数据 ---');
      console.log('资源名称:', entry.name);
      console.log('资源类型:', entry.initiatorType); // 例如 'img', 'script', 'link', 'xmlhttprequest'
      console.log('加载耗时:', entry.duration.toFixed(2), 'ms');
      console.log('资源大小 (decodedBodySize):', (entry.decodedBodySize / 1024).toFixed(2), 'KB');
      console.log('完整资源条目:', entry);

      // 可以在这里收集慢加载的资源信息
      if (entry.duration > 200) { // 假设超过200ms认为是慢资源
        console.warn(`[慢资源] ${entry.name} (${entry.initiatorType}) 加载耗时 ${entry.duration.toFixed(2)}ms`);
      }
    }
  });
});

resourceObserver.observe({ entryTypes: ['resource'] });

console.log('PerformanceObserver for resource timing started.');

解释:

  • entry.name: 资源的 URL。
  • entry.initiatorType: 触发资源加载的类型,例如 img (图片), script (JavaScript), link (CSS), xmlhttprequest (XHR) 等。
  • entry.duration: 资源从开始请求到完全加载的总耗时。
  • entry.decodedBodySize: 资源解码后的实际大小(字节)。

示例 3: 监控渲染时间 (Paint Timing - FCP, LCP)

paint 类型用于测量页面渲染的关键时刻,最常用的是 First Contentful Paint (FCP) 和 Largest Contentful Paint (LCP)。

js 复制代码
const paintObserver = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntriesByType('paint');

  entries.forEach((entry) => {
    console.log('--- 渲染性能数据 ---');
    console.log('名称:', entry.name); // 'first-contentful-paint' 或 'largest-contentful-paint'
    console.log('时间:', entry.startTime.toFixed(2), 'ms'); // 页面加载开始到该事件发生的时间

    if (entry.name === 'first-contentful-paint') {
      console.log('FCP (首次内容绘制):', entry.startTime.toFixed(2), 'ms');
      // 可以在这里发送 FCP 数据
      // sendToAnalyticsService({ type: 'fcp', value: entry.startTime });
    } else if (entry.name === 'largest-contentful-paint') {
      console.log('LCP (最大内容绘制):', entry.startTime.toFixed(2), 'ms');
      // LCP 可能会多次触发,直到页面稳定,通常取最后一个有效值
      // sendToAnalyticsService({ type: 'lcp', value: entry.startTime });
    }
  });

  // 注意:LCP 可能会多次触发,直到页面布局稳定。
  // 如果只需要最终的 LCP 值,通常在页面加载完成后(例如 load 事件后)再处理收集到的 LCP 数据。
  // 或者在 LCP 规范中,LCP 的值是最后一个 LCP entry 的 startTime。
  // 对于 Core Web Vitals,通常在页面卸载前或用户不再活跃时发送最终值。
});

// 观察 'paint' 类型,并启用 buffered 选项以获取历史事件
paintObserver.observe({ entryTypes: ['paint'], buffered: true });

console.log('PerformanceObserver for paint timing (FCP, LCP) started.');

解释:

  • first-contentful-paint: 浏览器首次渲染任何文本、图像(包括非背景图像)、非白色 <canvas> 或 SVG 的时间。
  • largest-contentful-paint: 页面上最大的内容元素(通常是图片或大块文本)在视口中变得可见的时间。LCP 可能会在页面加载过程中多次更新,直到最终确定。

示例 4: 监控长任务 (Long Tasks)

长任务是指在主线程上运行时间超过 50 毫秒的 JavaScript 任务。它们会阻塞主线程,导致页面无响应,影响用户体验。

js 复制代码
const longTaskObserver = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntriesByType('longtask');

  entries.forEach((entry) => {
    console.log('--- 发现长任务 ---');
    console.log('任务耗时:', entry.duration.toFixed(2), 'ms');
    console.log('开始时间:', entry.startTime.toFixed(2), 'ms');
    console.log('任务名称:', entry.name); // 通常是 'self' 或 'script'
    console.log('来源:', entry.attribution); // 包含导致长任务的代码信息
    console.log('完整长任务条目:', entry);

    // 可以根据耗时阈值进行报警或上报
    if (entry.duration > 100) { // 超过100ms的长任务
      console.error(`[警告] 发现严重长任务: ${entry.duration.toFixed(2)}ms`);
      // sendToAnalyticsService({
      //   type: 'longtask',
      //   duration: entry.duration,
      //   startTime: entry.startTime,
      //   attribution: entry.attribution
      // });
    }
  });
});

longTaskObserver.observe({ entryTypes: ['longtask'] });

console.log('PerformanceObserver for long tasks started.');

// 模拟一个长任务来测试
function simulateLongTask() {
  let sum = 0;
  for (let i = 0; i < 1000000000; i++) { // 模拟一个耗时操作
    sum += i;
  }
  console.log('模拟长任务完成,结果:', sum);
}

// 可以在某个用户交互或初始化时调用
// setTimeout(simulateLongTask, 1000);

解释:

  • entry.duration: 长任务的持续时间。
  • entry.attribution: 这是一个数组,提供了关于长任务来源的更多上下文信息,例如是哪个脚本文件、哪个函数导致的。这对于调试非常有用。

示例 5: 监控布局偏移 (Layout Shifts - CLS)

Cumulative Layout Shift (CLS) 测量页面内容的意外移动量,是 Core Web Vitals 的一个重要指标。

js 复制代码
let cumulativeLayoutShiftScore = 0;

const clsObserver = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntriesByType('layout-shift');

  entries.forEach((entry) => {
    // 排除非用户输入的布局偏移 (如动画)
    if (!entry.hadRecentInput) {
      cumulativeLayoutShiftScore += entry.value;
      console.log('--- 布局偏移 ---');
      console.log('偏移值:', entry.value.toFixed(4));
      console.log('当前 CLS:', cumulativeLayoutShiftScore.toFixed(4));
      console.log('完整布局偏移条目:', entry);

      // 可以在这里实时更新 CLS 指标或在页面卸载前发送最终值
    }
  });
});

// 观察 'layout-shift' 类型,并启用 buffered 选项以获取历史事件
clsObserver.observe({ entryTypes: ['layout-shift'], buffered: true });

console.log('PerformanceObserver for layout shifts (CLS) started.');

// 在页面卸载前发送最终 CLS 值
window.addEventListener('beforeunload', () => {
  console.log('最终 CLS 值:', cumulativeLayoutShiftScore.toFixed(4));
  // sendToAnalyticsService({ type: 'cls', value: cumulativeLayoutShiftScore });
});

解释:

  • entry.value: 单次布局偏移的得分。
  • entry.hadRecentInput: 如果此布局偏移是由于用户最近的输入(如点击按钮导致的新内容加载)引起的,则为 true。通常,我们只关心非用户输入导致的意外布局偏移。
  • cumulativeLayoutShiftScore: 累积布局偏移得分,是所有非用户输入导致的布局偏移值的总和。

示例 6: 监控元素渲染时间 (Element Timing)

element 类型允许你观察特定元素的渲染时间,这对于更精确地测量 LCP 或其他关键元素的加载时间非常有用。

js 复制代码
// 在 HTML 中添加一个 id="my-lcp-element" 的元素,例如一个大图或主标题
// <img id="my-lcp-element" src="large-image.jpg" alt="Large Content" style="width: 80vw; height: auto;">

const elementObserver = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntriesByType('element');

  entries.forEach((entry) => {
    console.log('--- 元素渲染时间 ---');
    console.log('元素名称:', entry.name); // 通常是元素的 ID 或其他标识
    console.log('渲染时间:', entry.renderTime.toFixed(2), 'ms'); // 元素在屏幕上渲染的时间
    console.log('加载时间:', entry.loadTime.toFixed(2), 'ms');   // 元素完全加载的时间
    console.log('元素ID:', entry.element.id);
    console.log('完整元素条目:', entry);

    // 如果这个元素是你的 LCP 候选元素,可以比较其 renderTime
    if (entry.element.id === 'my-lcp-element') {
      console.log(`LCP 候选元素 (${entry.element.id}) 渲染时间: ${entry.renderTime.toFixed(2)}ms`);
    }
  });
});

// 观察 'element' 类型,并启用 buffered 选项
elementObserver.observe({ entryTypes: ['element'], buffered: true });

console.log('PerformanceObserver for element timing started.');

解释:

  • 要使用 element 类型,你需要在 HTML 元素上添加 elementtiming="meaningful" 属性,并且可以添加 idname 属性来识别它。

    ini 复制代码
    <img src="your-image.jpg" elementtiming="meaningful" id="main-hero-image">
  • entry.renderTime: 元素在屏幕上首次绘制的时间。

  • entry.loadTime: 元素完全加载完成的时间(例如图片完全解码)。

  • entry.element: 对被观察 DOM 元素的引用。

重要注意事项

  1. buffered: true 选项: 对于在 PerformanceObserver 实例化之前可能已经发生的事件(如 navigation, paint, layout-shift),务必设置 { buffered: true },否则你可能会错过这些初始事件。

  2. disconnect() 方法: 当你不再需要收集性能数据时,调用 observer.disconnect() 来停止观察并释放资源。例如,在单页应用中,当组件卸载时可以断开观察。

  3. 浏览器兼容性: PerformanceObserver 及其支持的 entryTypes 在不同浏览器中可能存在差异。例如,layout-shiftelement 是相对较新的 API。在使用前最好查阅 MDN 或 Can I use。

  4. 数据上报: 收集到的性能数据通常需要发送到后端服务或第三方分析平台(如 Google Analytics, Sentry, 自建监控系统)进行存储、分析和可视化。

  5. performance.getEntriesByType() 的区别:

    • performance.getEntriesByType() 只能获取当前时间点之前已经发生的性能条目,是同步的。
    • PerformanceObserver 是异步的,它会在新的性能条目发生时通知你,这使得它更适合实时监控和长生命周期的应用。对于 Core Web Vitals 这样的指标,PerformanceObserver 是首选。

通过合理使用 PerformanceObserver,你可以更深入地理解前端应用的性能表现,并为优化提供数据支持。

相关推荐
我不吃饼干2 小时前
在 React 中实现倒计时功能会有什么坑
前端·react.js
王者鳜錸2 小时前
PYTHON从入门到实践-18Django从零开始构建Web应用
前端·python·sqlite
拾光拾趣录2 小时前
ES6到HTTPS全链路连环拷问,99%人第3题就翻车?
前端·面试
haaaaaaarry3 小时前
Element Plus常见基础组件(二)
开发语言·前端·javascript
xyphf_和派孔明3 小时前
关于echarts的性能优化考虑
前端·性能优化·echarts
PyHaVolask4 小时前
HTML 表单进阶:用户体验优化与实战应用
前端·javascript·html·用户体验
A了LONE4 小时前
cv弹窗,退款确认弹窗
java·服务器·前端
AntBlack4 小时前
闲谈 :AI 生成视频哪家强 ,掘友们有没有推荐的工具?
前端·后端·aigc
花菜会噎住5 小时前
Vue3核心语法进阶(computed与监听)
前端·javascript·vue.js