PerformanceObserver
是 Web Performance API 的一部分,它提供了一种异步、高效地收集浏览器性能指标的方式。与传统的 performance.getEntries()
或 performance.getEntriesByType()
方法不同,PerformanceObserver
允许你订阅特定类型的性能事件,并在这些事件发生时获得通知,而不是在某个时间点一次性获取所有已发生的事件。这对于实时监控和报告性能数据非常有用,尤其是在单页应用 (SPA) 中,因为页面加载后的用户交互也会产生新的性能事件。
PerformanceObserver
的作用
PerformanceObserver
主要用于:
- 监控页面加载性能: 获取导航时间、资源加载时间等。
- 跟踪用户体验指标: 例如 First Contentful Paint (FCP)、Largest Contentful Paint (LCP)、Cumulative Layout Shift (CLS) 等 Core Web Vitals 指标。
- 识别性能瓶颈: 发现长时间运行的任务 (Long Tasks) 或渲染阻塞。
- 实时数据收集: 异步获取性能数据,方便发送到分析服务。
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
: 表示从fetchStart
到loadEventEnd
的总时间,通常被认为是页面加载的总耗时。buffered: true
: 这个选项非常重要。对于某些只发生一次或在PerformanceObserver
实例化之前就可能发生的事件(如navigation
和paint
),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"
属性,并且可以添加id
或name
属性来识别它。ini<img src="your-image.jpg" elementtiming="meaningful" id="main-hero-image">
-
entry.renderTime
: 元素在屏幕上首次绘制的时间。 -
entry.loadTime
: 元素完全加载完成的时间(例如图片完全解码)。 -
entry.element
: 对被观察 DOM 元素的引用。
重要注意事项
-
buffered: true
选项: 对于在PerformanceObserver
实例化之前可能已经发生的事件(如navigation
,paint
,layout-shift
),务必设置{ buffered: true }
,否则你可能会错过这些初始事件。 -
disconnect()
方法: 当你不再需要收集性能数据时,调用observer.disconnect()
来停止观察并释放资源。例如,在单页应用中,当组件卸载时可以断开观察。 -
浏览器兼容性:
PerformanceObserver
及其支持的entryTypes
在不同浏览器中可能存在差异。例如,layout-shift
和element
是相对较新的 API。在使用前最好查阅 MDN 或 Can I use。 -
数据上报: 收集到的性能数据通常需要发送到后端服务或第三方分析平台(如 Google Analytics, Sentry, 自建监控系统)进行存储、分析和可视化。
-
与
performance.getEntriesByType()
的区别:performance.getEntriesByType()
只能获取当前时间点之前已经发生的性能条目,是同步的。PerformanceObserver
是异步的,它会在新的性能条目发生时通知你,这使得它更适合实时监控和长生命周期的应用。对于 Core Web Vitals 这样的指标,PerformanceObserver
是首选。
通过合理使用 PerformanceObserver
,你可以更深入地理解前端应用的性能表现,并为优化提供数据支持。