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 性能优化

  1. 只观察必要的条目类型

    • 根据需求选择要观察的条目类型
    • 避免观察所有类型,这会增加性能开销
    • 只在需要时启用观察
  2. 合理处理性能数据

    • 定期清理旧的性能条目
    • 避免在回调函数中执行复杂计算
    • 考虑采样处理大量数据
  3. 生产环境使用

    • 在生产环境中,考虑只发送关键性能指标到服务器
    • 使用采样率减少数据量
    • 避免在生产环境中输出大量日志
  4. 结合其他性能工具

    • 与 Chrome DevTools 结合使用进行调试
    • 与 Lighthouse 结合进行性能审计
    • 与 RUM (Real User Monitoring) 工具结合使用

4.2 代码组织

  1. 使用组合式函数封装

    • 将 Performance Observer 逻辑封装到可复用的组合式函数中
    • 提供清晰的 API 接口
    • 自动处理组件生命周期
  2. 分类处理不同类型的性能数据

    • 为不同类型的性能条目编写专门的处理逻辑
    • 提供数据可视化组件
    • 实现性能数据的过滤和排序
  3. 错误处理

    • 处理 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 深入学习

6.3 相关工具和库

7. 实践练习

7.1 练习 1:实现组件性能监控

目标:使用 Performance Observer API 监控 Vue 3 组件的渲染性能

要求

  1. 创建一个可复用的组合式函数,用于监控组件的挂载和更新性能
  2. 在多个组件中使用该组合式函数
  3. 实现性能数据的可视化展示
  4. 支持性能数据的导出和分析
  5. 实现性能阈值报警

7.2 练习 2:实现资源加载监控

目标:监控 Vue 3 应用中资源的加载性能

要求

  1. 使用 Performance Observer API 监控所有资源的加载
  2. 实现资源加载时间的统计和分析
  3. 识别加载缓慢的资源
  4. 实现资源加载瀑布图
  5. 提供优化建议

7.3 练习 3:实现长任务监控

目标:使用 Performance Observer API 监控长任务,优化用户体验

要求

  1. 监控应用中的长任务
  2. 分析长任务的来源和影响
  3. 实现长任务可视化
  4. 提供优化建议
  5. 实现长任务报警机制

8. 总结

Performance Observer API 是一个强大的工具,用于监控和分析网页性能数据。在 Vue 3 应用中,它可以帮助我们深入了解应用的性能状况,识别性能瓶颈,优化用户体验。

通过创建可复用的组合式函数,我们可以将 Performance Observer 的复杂性封装起来,提供简洁的 API 供组件使用。同时,我们需要注意性能优化、合理处理组件生命周期,以确保应用的高效运行。

在实际开发中,Performance Observer API 可以与其他现代浏览器 API 结合使用,实现更全面的性能监控和分析。通过持续监控和优化,我们可以构建更快速、更流畅的 Vue 3 应用。

9. 代码示例下载

10. 后续学习建议

  1. 学习 Web Vitals 指标及其测量方法
  2. 深入研究浏览器渲染原理
  3. 学习性能优化的最佳实践
  4. 探索 RUM (Real User Monitoring) 工具的使用
  5. 学习如何使用 Performance Observer API 进行自动化性能测试

通过深入学习和实践 Performance Observer API,你将能够构建更高效、更优化的 Vue 3 应用,提供更好的用户体验。

« 上一篇 Vue 3 与 Mutation Observer API 下一篇 » Vue 3 与 Network Information API