Vue 3 与 Performance Observer API
1. 概述
Performance Observer API 是一个现代浏览器 API,用于监控和分析网页性能数据。它提供了一种高效、异步的方式来获取和处理性能指标,如页面加载时间、资源加载、渲染性能、用户交互延迟等。
在 Vue 3 应用中,Performance Observer API 可以用于:
- 监控组件渲染性能
- 分析资源加载时间
- 检测长任务(Long Tasks)
- 跟踪导航性能
- 测量用户交互延迟
- 实现性能监控和报警
- 优化应用性能
2. 核心知识
2.1 Performance Observer 基础
Performance Observer API 允许你监控以下类型的性能条目:
| 条目类型 | 描述 |
|---|---|
navigation |
页面导航性能数据 |
resource |
资源加载性能数据 |
mark |
自定义标记点 |
measure |
自定义测量时间段 |
frame |
帧渲染性能数据 |
longtask |
长任务数据 |
paint |
绘制性能数据 |
layout-shift |
布局偏移数据 |
element |
元素渲染性能数据 |
2.2 Performance Observer 构造函数
const observer = new PerformanceObserver(callback);其中 callback 是一个函数,当观察到性能条目时被调用:
const callback = (list, observer) => {
for (const entry of list.getEntries()) {
// 处理每个性能条目
}
};2.3 观察性能条目
// 观察指定类型的性能条目
observer.observe({ entryTypes: ['navigation', 'resource', 'longtask'] });
// 观察特定名称的标记和测量
observer.observe({
entryTypes: ['mark', 'measure'],
buffered: true // 包括过去的条目
});2.4 停止观察和清理
// 停止观察所有类型
observer.disconnect();
// 获取未处理的条目
const entries = observer.takeRecords();3. Vue 3 与 Performance Observer 集成
3.1 基础使用示例
<template>
<div>
<h2>Performance Observer 示例</h2>
<div class="controls">
<button @click="startObserving">开始观察</button>
<button @click="stopObserving">停止观察</button>
<button @click="clearEntries">清空条目</button>
<button @click="addCustomMark">添加自定义标记</button>
<button @click="measureCustom">添加自定义测量</button>
</div>
<div class="entries">
<h3>性能条目 (共 {{ performanceEntries.length }} 条):</h3>
<div v-if="performanceEntries.length === 0" class="empty">暂无性能条目</div>
<div v-else class="entries-container">
<div
v-for="(entry, index) in performanceEntries"
:key="index"
class="entry-item"
:class="`entry-${entry.entryType}`"
>
<div class="entry-header">
<strong>类型:{{ entry.entryType }}</strong>
<span class="entry-name">{{ entry.name || 'N/A' }}</span>
</div>
<div class="entry-details">
<div v-if="entry.startTime !== undefined" class="detail-item">
<span class="label">开始时间:</span>
<span class="value">{{ entry.startTime.toFixed(2) }}ms</span>
</div>
<div v-if="entry.duration !== undefined" class="detail-item">
<span class="label">持续时间:</span>
<span class="value">{{ entry.duration.toFixed(2) }}ms</span>
</div>
<div v-if="entry.entryType === 'resource'" class="detail-item">
<span class="label">资源类型:</span>
<span class="value">{{ entry.initiatorType }}</span>
</div>
<div v-if="entry.entryType === 'navigation'" class="detail-item">
<span class="label">导航类型:</span>
<span class="value">{{ entry.type }}</span>
</div>
<div v-if="entry.entryType === 'longtask'" class="detail-item">
<span class="label">任务源:</span>
<span class="value">{{ entry.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const performanceEntries = ref([]);
let observer = null;
let isObserving = ref(false);
let customMarkCount = 0;
// 开始观察
const startObserving = () => {
if (isObserving.value) return;
// 创建 Performance Observer
observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
performanceEntries.value = [...performanceEntries.value, ...entries];
// 只保留最近100条条目
if (performanceEntries.value.length > 100) {
performanceEntries.value = performanceEntries.value.slice(-100);
}
});
// 观察多种性能条目类型
observer.observe({
entryTypes: [
'navigation',
'resource',
'mark',
'measure',
'longtask',
'paint',
'layout-shift'
],
buffered: true // 包括过去的条目
});
isObserving.value = true;
};
// 停止观察
const stopObserving = () => {
if (!isObserving.value || !observer) return;
observer.disconnect();
observer = null;
isObserving.value = false;
};
// 清空条目
const clearEntries = () => {
performanceEntries.value = [];
};
// 添加自定义标记
const addCustomMark = () => {
const markName = `custom-mark-${++customMarkCount}`;
performance.mark(markName);
};
// 添加自定义测量
const measureCustom = () => {
const markName1 = `measure-start-${Date.now()}`;
const markName2 = `measure-end-${Date.now()}`;
const measureName = `custom-measure-${Date.now()}`;
// 创建标记
performance.mark(markName1);
// 模拟一些工作
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
performance.mark(markName2);
// 测量两个标记之间的时间
performance.measure(measureName, markName1, markName2);
};
// 组件挂载时自动开始观察
onMounted(() => {
startObserving();
});
// 组件卸载时自动停止观察
onUnmounted(() => {
stopObserving();
});
</script>
<style scoped>
.controls {
margin: 20px 0;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
button {
padding: 8px 16px;
cursor: pointer;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
transition: background-color 0.3s;
}
button:hover {
background-color: #35495e;
}
.entries {
margin-top: 20px;
}
.empty {
text-align: center;
color: #666;
padding: 20px;
border: 1px solid #eee;
border-radius: 4px;
}
.entries-container {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 500px;
overflow-y: auto;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
}
.entry-item {
padding: 12px;
border-radius: 4px;
background-color: #f9f9f9;
border-left: 4px solid #42b883;
transition: all 0.3s;
}
.entry-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.entry-navigation {
border-left-color: #42b883;
}
.entry-resource {
border-left-color: #35495e;
}
.entry-mark {
border-left-color: #ff9f43;
}
.entry-measure {
border-left-color: #1982c4;
}
.entry-longtask {
border-left-color: #ff6b6b;
}
.entry-paint {
border-left-color: #6a0572;
}
.entry-layout-shift {
border-left-color: #4ecdc4;
}
.entry-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.entry-name {
font-size: 0.9em;
color: #666;
background-color: #eee;
padding: 2px 8px;
border-radius: 12px;
}
.entry-details {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.9em;
}
.detail-item {
display: flex;
justify-content: space-between;
}
.label {
color: #666;
}
.value {
font-weight: bold;
color: #333;
}
</style>3.2 创建可复用的 Performance Observer 组合式函数
// src/composables/usePerformanceObserver.js
import { ref, onMounted, onUnmounted } from 'vue';
export function usePerformanceObserver(options = {}) {
const entries = ref([]);
const isObserving = ref(false);
let observer = null;
// 默认配置
const defaultOptions = {
entryTypes: ['navigation', 'resource', 'longtask'],
buffered: true
};
const mergedOptions = { ...defaultOptions, ...options };
// 创建观察器
const createObserver = () => {
if (observer) {
observer.disconnect();
}
observer = new PerformanceObserver((list) => {
const newEntries = list.getEntries();
entries.value = [...entries.value, ...newEntries];
// 只保留最近200条条目
if (entries.value.length > 200) {
entries.value = entries.value.slice(-200);
}
});
};
// 开始观察
const startObserving = () => {
if (isObserving.value) return;
createObserver();
observer.observe(mergedOptions);
isObserving.value = true;
};
// 停止观察
const stopObserving = () => {
if (!isObserving.value || !observer) return;
observer.disconnect();
observer = null;
isObserving.value = false;
};
// 清空条目
const clearEntries = () => {
entries.value = [];
};
// 获取未处理的条目
const takeRecords = () => {
if (observer) {
const records = observer.takeRecords();
entries.value = [...entries.value, ...records];
return records;
}
return [];
};
// 添加自定义标记
const mark = (name) => {
performance.mark(name);
};
// 添加自定义测量
const measure = (name, startMark, endMark) => {
performance.measure(name, startMark, endMark);
};
// 清除标记
const clearMarks = (name) => {
performance.clearMarks(name);
};
// 清除测量
const clearMeasures = (name) => {
performance.clearMeasures(name);
};
// 组件挂载时自动开始观察
onMounted(() => {
startObserving();
});
// 组件卸载时自动停止观察
onUnmounted(() => {
stopObserving();
});
return {
entries,
isObserving,
startObserving,
stopObserving,
clearEntries,
takeRecords,
mark,
measure,
clearMarks,
clearMeasures
};
}3.3 使用组合式函数的示例
<template>
<div>
<h2>使用 usePerformanceObserver 组合式函数</h2>
<!-- 组件性能监控 -->
<div class="component-section">
<h3>组件渲染性能监控</h3>
<button @click="renderHeavyComponent">渲染复杂组件</button>
<div ref="componentContainer" class="component-container"></div>
<div class="component-stats">
<p>渲染时间:{{ renderTime }}ms</p>
</div>
</div>
<!-- 资源加载监控 -->
<div class="resource-section">
<h3>资源加载监控</h3>
<button @click="loadImage">加载图片资源</button>
<div class="image-container">
<img v-for="img in images" :key="img" :src="img" :alt="'Loaded image'" />
</div>
</div>
<!-- 性能条目列表 -->
<div class="entries-section">
<h3>性能条目 (共 {{ entries.length }} 条):</h3>
<div class="controls">
<button @click="clearEntries">清空条目</button>
<button @click="toggleObserving">{{ isObserving ? '停止观察' : '开始观察' }}</button>
</div>
<div class="entries-list">
<div
v-for="(entry, index) in filteredEntries"
:key="index"
class="entry-item"
>
<strong>{{ entry.entryType }}</strong>: {{ entry.name }} - {{ entry.duration.toFixed(2) }}ms
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, h } from 'vue';
import { usePerformanceObserver } from './composables/usePerformanceObserver';
const componentContainer = ref(null);
const renderTime = ref(0);
const images = ref([]);
// 使用组合式函数
const {
entries,
isObserving,
startObserving,
stopObserving,
clearEntries,
mark,
measure
} = usePerformanceObserver({
entryTypes: ['navigation', 'resource', 'longtask', 'mark', 'measure', 'paint'],
buffered: true
});
// 过滤显示的条目
const filteredEntries = computed(() => {
return entries.value.slice(-20); // 只显示最近20条
});
// 切换观察状态
const toggleObserving = () => {
if (isObserving.value) {
stopObserving();
} else {
startObserving();
}
};
// 渲染复杂组件
const renderHeavyComponent = () => {
const startMark = `render-start-${Date.now()}`;
mark(startMark);
// 清空容器
componentContainer.value.innerHTML = '';
// 创建一个复杂组件
const HeavyComponent = {
render() {
return h('div', {
class: 'heavy-component'
}, [
h('h4', '复杂组件'),
h('div', {
class: 'content'
}, Array.from({ length: 1000 }, (_, i) =>
h('div', {
key: i,
class: 'item'
}, `项目 ${i + 1}`)
))
]);
}
};
// 渲染组件
const vnode = h(HeavyComponent);
// 这里简化了渲染过程,实际项目中会使用 Vue 的渲染函数
const mountHeavyComponent = () => {
const endMark = `render-end-${Date.now()}`;
mark(endMark);
// 测量渲染时间
measure('component-render', startMark, endMark);
// 获取测量结果
const measures = performance.getEntriesByName('component-render');
if (measures.length > 0) {
renderTime.value = measures[measures.length - 1].duration.toFixed(2);
}
};
// 模拟异步渲染
setTimeout(() => {
mountHeavyComponent();
}, 0);
};
// 加载图片资源
const loadImage = () => {
const imageUrl = `https://picsum.photos/800/600?random=${Date.now()}`;
images.value.push(imageUrl);
};
</script>
<style scoped>
.component-section,
.resource-section,
.entries-section {
margin: 20px 0;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
}
.component-container {
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
min-height: 100px;
overflow: auto;
}
.heavy-component {
padding: 10px;
background-color: #f0f8ff;
border: 1px solid #add8e6;
}
.heavy-component .content {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin-top: 10px;
}
.heavy-component .item {
padding: 5px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.8em;
}
.image-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 10px 0;
}
.image-container img {
max-width: 200px;
max-height: 150px;
object-fit: cover;
border: 1px solid #ddd;
border-radius: 4px;
}
.controls {
margin: 10px 0;
}
button {
margin-right: 10px;
padding: 8px 16px;
cursor: pointer;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
transition: background-color 0.3s;
}
button:hover {
background-color: #35495e;
}
.entries-list {
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #eee;
padding: 10px;
border-radius: 4px;
}
.entry-item {
padding: 8px;
margin: 5px 0;
background-color: #f9f9f9;
border-radius: 4px;
font-size: 0.9em;
}
.component-stats {
margin-top: 10px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
</style>3.4 监控 Vue 3 组件渲染性能
// src/composables/useComponentPerformance.js
import { onMounted, onUnmounted } from 'vue';
export function useComponentPerformance(componentName) {
let mountStart = 0;
let updateStart = 0;
// 监控组件挂载
const markMountStart = () => {
mountStart = performance.now();
};
const markMountEnd = () => {
const duration = performance.now() - mountStart;
performance.mark(`${componentName}-mount-start`);
performance.mark(`${componentName}-mount-end`);
performance.measure(`${componentName}-mount`, `${componentName}-mount-start`, `${componentName}-mount-end`);
console.log(`${componentName} 挂载时间: ${duration.toFixed(2)}ms`);
};
// 监控组件更新
const markUpdateStart = () => {
updateStart = performance.now();
};
const markUpdateEnd = () => {
const duration = performance.now() - updateStart;
performance.mark(`${componentName}-update-start`);
performance.mark(`${componentName}-update-end`);
performance.measure(`${componentName}-update`, `${componentName}-update-start`, `${componentName}-update-end`);
console.log(`${componentName} 更新时间: ${duration.toFixed(2)}ms`);
};
return {
markMountStart,
markMountEnd,
markUpdateStart,
markUpdateEnd
};
}使用示例:
<template>
<div class="my-component">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
<button @click="updateContent">更新内容</button>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useComponentPerformance } from './composables/useComponentPerformance';
const title = ref('性能监控组件');
const content = ref('初始内容');
// 使用组件性能监控
const {
markMountStart,
markMountEnd,
markUpdateStart,
markUpdateEnd
} = useComponentPerformance('MyComponent');
// 标记挂载开始
markMountStart();
// 组件挂载时标记结束
onMounted(() => {
markMountEnd();
});
// 监听内容变化,标记更新性能
watch([title, content], () => {
markUpdateStart();
// 模拟一些更新工作
setTimeout(() => {
markUpdateEnd();
}, 0);
});
// 更新内容
const updateContent = () => {
content.value = `更新后的内容 ${Date.now()}`;
};
</script>4. 最佳实践
4.1 性能优化
只观察必要的条目类型
- 根据需求选择要观察的条目类型
- 避免观察所有类型,这会增加性能开销
- 只在需要时启用观察
合理处理性能数据
- 定期清理旧的性能条目
- 避免在回调函数中执行复杂计算
- 考虑采样处理大量数据
生产环境使用
- 在生产环境中,考虑只发送关键性能指标到服务器
- 使用采样率减少数据量
- 避免在生产环境中输出大量日志
结合其他性能工具
- 与 Chrome DevTools 结合使用进行调试
- 与 Lighthouse 结合进行性能审计
- 与 RUM (Real User Monitoring) 工具结合使用
4.2 代码组织
使用组合式函数封装
- 将 Performance Observer 逻辑封装到可复用的组合式函数中
- 提供清晰的 API 接口
- 自动处理组件生命周期
分类处理不同类型的性能数据
- 为不同类型的性能条目编写专门的处理逻辑
- 提供数据可视化组件
- 实现性能数据的过滤和排序
错误处理
- 处理 Performance Observer 可能抛出的异常
- 确保在不支持的浏览器中优雅降级
4.3 浏览器兼容性
Performance Observer API 具有良好的浏览器支持:
- Chrome 52+
- Firefox 57+
- Safari 11+
- Edge 79+
对于不支持的浏览器,可以使用以下方式处理:
if ('PerformanceObserver' in window) {
// 使用 Performance Observer API
} else {
// 优雅降级,使用传统的性能 API 或不进行监控
console.log('Performance Observer API not supported');
}5. 常见问题与解决方案
5.1 观察器不触发
问题:Performance Observer 没有检测到预期的性能条目
解决方案:
- 检查是否使用了正确的
entryTypes - 确认观察的条目类型是否被浏览器支持
- 检查是否设置了
buffered: true以包括过去的条目 - 确认在观察前是否已经发生了性能事件
5.2 性能数据不准确
问题:Performance Observer 报告的性能数据不准确
解决方案:
- 确保在正确的时机调用
mark()和measure() - 考虑浏览器时间精度限制
- 避免在性能关键路径上执行大量监控代码
- 使用多个测量点验证数据准确性
5.3 性能开销过大
问题:使用 Performance Observer 导致应用性能下降
解决方案:
- 减少观察的条目类型数量
- 降低数据采样率
- 定期清理旧的性能条目
- 避免在回调函数中执行复杂操作
- 只在必要时启用观察
5.4 不支持某些条目类型
问题:某些性能条目类型不被支持
解决方案:
- 检查浏览器兼容性
- 使用特性检测确认支持的条目类型
- 为不支持的类型提供降级方案
- 考虑使用 polyfill
6. 高级学习资源
6.1 官方文档
6.2 深入学习
- Web Performance API
- Performance Monitoring with Performance Observer
- Measuring Performance with the User Timing API
6.3 相关工具和库
7. 实践练习
7.1 练习 1:实现组件性能监控
目标:使用 Performance Observer API 监控 Vue 3 组件的渲染性能
要求:
- 创建一个可复用的组合式函数,用于监控组件的挂载和更新性能
- 在多个组件中使用该组合式函数
- 实现性能数据的可视化展示
- 支持性能数据的导出和分析
- 实现性能阈值报警
7.2 练习 2:实现资源加载监控
目标:监控 Vue 3 应用中资源的加载性能
要求:
- 使用 Performance Observer API 监控所有资源的加载
- 实现资源加载时间的统计和分析
- 识别加载缓慢的资源
- 实现资源加载瀑布图
- 提供优化建议
7.3 练习 3:实现长任务监控
目标:使用 Performance Observer API 监控长任务,优化用户体验
要求:
- 监控应用中的长任务
- 分析长任务的来源和影响
- 实现长任务可视化
- 提供优化建议
- 实现长任务报警机制
8. 总结
Performance Observer API 是一个强大的工具,用于监控和分析网页性能数据。在 Vue 3 应用中,它可以帮助我们深入了解应用的性能状况,识别性能瓶颈,优化用户体验。
通过创建可复用的组合式函数,我们可以将 Performance Observer 的复杂性封装起来,提供简洁的 API 供组件使用。同时,我们需要注意性能优化、合理处理组件生命周期,以确保应用的高效运行。
在实际开发中,Performance Observer API 可以与其他现代浏览器 API 结合使用,实现更全面的性能监控和分析。通过持续监控和优化,我们可以构建更快速、更流畅的 Vue 3 应用。
9. 代码示例下载
10. 后续学习建议
- 学习 Web Vitals 指标及其测量方法
- 深入研究浏览器渲染原理
- 学习性能优化的最佳实践
- 探索 RUM (Real User Monitoring) 工具的使用
- 学习如何使用 Performance Observer API 进行自动化性能测试
通过深入学习和实践 Performance Observer API,你将能够构建更高效、更优化的 Vue 3 应用,提供更好的用户体验。