Performance API 完全指南 - Web 性能监控与分析实战

HTMLPAGE 团队
12 分钟阅读

深入讲解浏览器 Performance API 的使用方法,包括性能时间线、资源加载分析、用户体验指标测量。提供实际可运行的代码示例和最佳实践。

#Performance API #性能监控 #Web Vitals #前端优化

Performance API 完全指南

概述

Performance API 是浏览器提供的一套强大接口,用于精确测量 Web 应用的各项性能指标。通过它,开发者可以获取页面加载时间、资源下载耗时、用户交互延迟等关键数据,为性能优化提供数据支撑。

Performance API 核心概念

API 架构总览

Performance API 体系结构

window.performance
├── timing (已废弃,使用 Navigation Timing Level 2)
├── navigation
├── memory (Chrome)
├── timeOrigin
└── 方法
    ├── now() - 高精度时间戳
    ├── mark() - 创建标记
    ├── measure() - 测量时间段
    ├── getEntries() - 获取所有条目
    ├── getEntriesByType() - 按类型获取
    └── getEntriesByName() - 按名称获取

PerformanceEntry 类型
├── navigation - 页面导航信息
├── resource - 资源加载信息
├── paint - 绘制时间点
├── mark - 自定义标记
├── measure - 自定义测量
├── longtask - 长任务
├── largest-contentful-paint - LCP
├── first-input - FID
└── layout-shift - CLS

基础用法

// 获取高精度时间戳
const startTime = performance.now()

// 执行一些操作
doSomething()

const endTime = performance.now()
console.log(`操作耗时: ${endTime - startTime}ms`)

// 获取页面加载性能数据
const navEntry = performance.getEntriesByType('navigation')[0]
console.log('DOM 加载完成:', navEntry.domContentLoadedEventEnd)
console.log('页面完全加载:', navEntry.loadEventEnd)

// 获取所有资源加载数据
const resources = performance.getEntriesByType('resource')
resources.forEach(resource => {
  console.log(`${resource.name}: ${resource.duration}ms`)
})

导航时间线详解

// Navigation Timing Level 2 (推荐使用)
const [navEntry] = performance.getEntriesByType('navigation')

// 完整时间线
const navigationTimeline = {
  // DNS 查询
  dns: {
    start: navEntry.domainLookupStart,
    end: navEntry.domainLookupEnd,
    duration: navEntry.domainLookupEnd - navEntry.domainLookupStart
  },

  // TCP 连接
  tcp: {
    start: navEntry.connectStart,
    end: navEntry.connectEnd,
    duration: navEntry.connectEnd - navEntry.connectStart
  },

  // SSL/TLS 握手
  ssl: {
    start: navEntry.secureConnectionStart,
    end: navEntry.connectEnd,
    duration: navEntry.secureConnectionStart > 0 
      ? navEntry.connectEnd - navEntry.secureConnectionStart 
      : 0
  },

  // 请求/响应
  request: {
    start: navEntry.requestStart,
    end: navEntry.responseStart,
    duration: navEntry.responseStart - navEntry.requestStart
  },

  response: {
    start: navEntry.responseStart,
    end: navEntry.responseEnd,
    duration: navEntry.responseEnd - navEntry.responseStart
  },

  // DOM 处理
  domProcessing: {
    start: navEntry.responseEnd,
    end: navEntry.domContentLoadedEventEnd,
    duration: navEntry.domContentLoadedEventEnd - navEntry.responseEnd
  },

  // 页面完全加载
  pageLoad: {
    start: navEntry.startTime,
    end: navEntry.loadEventEnd,
    duration: navEntry.loadEventEnd - navEntry.startTime
  }
}

关键性能指标提取

// 提取关键性能指标
function getNavigationMetrics() {
  const [nav] = performance.getEntriesByType('navigation')
  
  if (!nav) return null

  return {
    // 重定向时间
    redirect: nav.redirectEnd - nav.redirectStart,
    
    // DNS 查询时间
    dns: nav.domainLookupEnd - nav.domainLookupStart,
    
    // TCP 连接时间
    tcp: nav.connectEnd - nav.connectStart,
    
    // 首字节时间 (TTFB)
    ttfb: nav.responseStart - nav.requestStart,
    
    // 内容下载时间
    download: nav.responseEnd - nav.responseStart,
    
    // DOM 解析时间
    domParse: nav.domInteractive - nav.responseEnd,
    
    // DOM 完成时间
    domComplete: nav.domComplete - nav.domInteractive,
    
    // DOMContentLoaded 时间
    domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
    
    // 页面完全加载时间
    load: nav.loadEventEnd - nav.startTime,
    
    // 传输类型
    transferSize: nav.transferSize,
    encodedBodySize: nav.encodedBodySize,
    decodedBodySize: nav.decodedBodySize
  }
}

// 使用示例
window.addEventListener('load', () => {
  // 确保所有数据都已记录
  setTimeout(() => {
    const metrics = getNavigationMetrics()
    console.table(metrics)
  }, 0)
})

Resource Timing API

资源加载分析

// 获取所有资源加载信息
function analyzeResources() {
  const resources = performance.getEntriesByType('resource')
  
  // 按类型分组
  const byType = resources.reduce((acc, r) => {
    const type = r.initiatorType
    if (!acc[type]) {
      acc[type] = { count: 0, totalTime: 0, totalSize: 0, resources: [] }
    }
    acc[type].count++
    acc[type].totalTime += r.duration
    acc[type].totalSize += r.transferSize || 0
    acc[type].resources.push({
      name: r.name,
      duration: r.duration,
      size: r.transferSize
    })
    return acc
  }, {})

  return byType
}

// 找出加载最慢的资源
function getSlowestResources(limit = 10) {
  const resources = performance.getEntriesByType('resource')
  
  return resources
    .sort((a, b) => b.duration - a.duration)
    .slice(0, limit)
    .map(r => ({
      name: r.name.split('/').pop(),
      fullUrl: r.name,
      duration: Math.round(r.duration),
      size: r.transferSize,
      type: r.initiatorType
    }))
}

// 资源加载时间线可视化数据
function getResourceTimeline() {
  const resources = performance.getEntriesByType('resource')
  
  return resources.map(r => ({
    name: r.name.split('/').pop(),
    startTime: r.startTime,
    duration: r.duration,
    endTime: r.startTime + r.duration,
    phases: {
      redirect: r.redirectEnd - r.redirectStart,
      dns: r.domainLookupEnd - r.domainLookupStart,
      tcp: r.connectEnd - r.connectStart,
      request: r.responseStart - r.requestStart,
      response: r.responseEnd - r.responseStart
    }
  }))
}

资源加载问题诊断

// 诊断资源加载问题
function diagnoseResourceIssues() {
  const resources = performance.getEntriesByType('resource')
  const issues = []

  resources.forEach(r => {
    // 问题 1: 慢 DNS 查询
    const dnsTime = r.domainLookupEnd - r.domainLookupStart
    if (dnsTime > 100) {
      issues.push({
        type: 'slow-dns',
        resource: r.name,
        time: dnsTime,
        suggestion: '考虑使用 DNS 预解析: <link rel="dns-prefetch">'
      })
    }

    // 问题 2: 慢 TCP 连接
    const tcpTime = r.connectEnd - r.connectStart
    if (tcpTime > 100) {
      issues.push({
        type: 'slow-tcp',
        resource: r.name,
        time: tcpTime,
        suggestion: '考虑使用 preconnect: <link rel="preconnect">'
      })
    }

    // 问题 3: 慢 TTFB
    const ttfb = r.responseStart - r.requestStart
    if (ttfb > 200) {
      issues.push({
        type: 'slow-ttfb',
        resource: r.name,
        time: ttfb,
        suggestion: '检查服务器响应时间,考虑使用 CDN'
      })
    }

    // 问题 4: 大文件
    if (r.transferSize > 500000) { // 500KB
      issues.push({
        type: 'large-file',
        resource: r.name,
        size: r.transferSize,
        suggestion: '考虑压缩或分割大文件'
      })
    }

    // 问题 5: 未压缩
    if (r.encodedBodySize && r.decodedBodySize) {
      const compressionRatio = r.encodedBodySize / r.decodedBodySize
      if (compressionRatio > 0.9 && r.decodedBodySize > 10000) {
        issues.push({
          type: 'not-compressed',
          resource: r.name,
          suggestion: '启用 Gzip/Brotli 压缩'
        })
      }
    }
  })

  return issues
}

Paint Timing API

获取绘制时间点

// 获取首次绘制和首次内容绘制时间
function getPaintMetrics() {
  const paintEntries = performance.getEntriesByType('paint')
  
  const metrics = {}
  
  paintEntries.forEach(entry => {
    if (entry.name === 'first-paint') {
      metrics.firstPaint = entry.startTime
    }
    if (entry.name === 'first-contentful-paint') {
      metrics.firstContentfulPaint = entry.startTime
    }
  })

  return metrics
}

// 使用 PerformanceObserver 实时监听
const paintObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    console.log(`${entry.name}: ${entry.startTime}ms`)
    
    // 发送到分析服务
    sendToAnalytics({
      metric: entry.name,
      value: entry.startTime
    })
  })
})

paintObserver.observe({ entryTypes: ['paint'] })

Core Web Vitals 测量

LCP (Largest Contentful Paint)

// 监测 LCP
function observeLCP() {
  let lcpValue = 0

  const lcpObserver = new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    // 取最后一个 LCP 条目(最终值)
    const lastEntry = entries[entries.length - 1]
    lcpValue = lastEntry.startTime

    console.log('LCP:', lcpValue, 'ms')
    console.log('LCP 元素:', lastEntry.element)
  })

  lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })

  // 页面隐藏时停止观察并上报
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
      lcpObserver.disconnect()
      reportMetric('LCP', lcpValue)
    }
  })
}

// LCP 评级
function rateLCP(value) {
  if (value <= 2500) return 'good'
  if (value <= 4000) return 'needs-improvement'
  return 'poor'
}

FID (First Input Delay)

// 监测 FID
function observeFID() {
  const fidObserver = new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    
    entries.forEach(entry => {
      const fidValue = entry.processingStart - entry.startTime
      
      console.log('FID:', fidValue, 'ms')
      console.log('输入类型:', entry.name)
      
      reportMetric('FID', fidValue)
    })
  })

  fidObserver.observe({ entryTypes: ['first-input'] })
}

// FID 评级
function rateFID(value) {
  if (value <= 100) return 'good'
  if (value <= 300) return 'needs-improvement'
  return 'poor'
}

CLS (Cumulative Layout Shift)

// 监测 CLS
function observeCLS() {
  let clsValue = 0
  let sessionValue = 0
  let sessionEntries = []

  const clsObserver = new PerformanceObserver((entryList) => {
    for (const entry of entryList.getEntries()) {
      // 只计算没有用户输入的布局偏移
      if (!entry.hadRecentInput) {
        const firstSessionEntry = sessionEntries[0]
        const lastSessionEntry = sessionEntries[sessionEntries.length - 1]

        // 如果与上一次偏移间隔不超过 1 秒,且与第一次偏移间隔不超过 5 秒
        // 则归入同一会话
        if (
          sessionValue &&
          entry.startTime - lastSessionEntry.startTime < 1000 &&
          entry.startTime - firstSessionEntry.startTime < 5000
        ) {
          sessionValue += entry.value
          sessionEntries.push(entry)
        } else {
          sessionValue = entry.value
          sessionEntries = [entry]
        }

        // 更新最大会话值
        if (sessionValue > clsValue) {
          clsValue = sessionValue
        }
      }
    }

    console.log('当前 CLS:', clsValue)
  })

  clsObserver.observe({ entryTypes: ['layout-shift'] })

  // 页面隐藏时上报
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
      clsObserver.disconnect()
      reportMetric('CLS', clsValue)
    }
  })
}

// CLS 评级
function rateCLS(value) {
  if (value <= 0.1) return 'good'
  if (value <= 0.25) return 'needs-improvement'
  return 'poor'
}

User Timing API

自定义性能标记

// 创建性能标记
performance.mark('app-init-start')

// 初始化应用
initializeApp()

performance.mark('app-init-end')

// 测量两个标记之间的时间
performance.measure('app-init-duration', 'app-init-start', 'app-init-end')

// 获取测量结果
const measures = performance.getEntriesByName('app-init-duration')
console.log('应用初始化耗时:', measures[0].duration, 'ms')

组件渲染性能追踪

// Vue/React 组件性能追踪示例
class PerformanceTracker {
  constructor() {
    this.marks = new Map()
  }

  // 开始追踪
  startTrack(name) {
    const markName = `${name}-start`
    performance.mark(markName)
    this.marks.set(name, markName)
  }

  // 结束追踪
  endTrack(name) {
    const startMark = this.marks.get(name)
    if (!startMark) return null

    const endMark = `${name}-end`
    performance.mark(endMark)

    const measureName = `${name}-duration`
    performance.measure(measureName, startMark, endMark)

    const [measure] = performance.getEntriesByName(measureName)
    
    // 清理
    performance.clearMarks(startMark)
    performance.clearMarks(endMark)
    performance.clearMeasures(measureName)
    this.marks.delete(name)

    return measure?.duration
  }

  // 追踪异步操作
  async trackAsync(name, asyncFn) {
    this.startTrack(name)
    try {
      const result = await asyncFn()
      const duration = this.endTrack(name)
      console.log(`${name}: ${duration}ms`)
      return result
    } catch (error) {
      this.endTrack(name)
      throw error
    }
  }
}

// 使用示例
const tracker = new PerformanceTracker()

// 追踪组件渲染
tracker.startTrack('UserList-render')
renderUserList()
const renderTime = tracker.endTrack('UserList-render')
console.log('UserList 渲染耗时:', renderTime)

// 追踪 API 调用
const users = await tracker.trackAsync('fetch-users', () => 
  fetch('/api/users').then(r => r.json())
)

Long Tasks API

监测长任务

// 监测长任务(超过 50ms 的任务)
const longTaskObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    console.log('检测到长任务:', {
      duration: entry.duration,
      startTime: entry.startTime,
      name: entry.name
    })

    // 如果任务超过 100ms,记录警告
    if (entry.duration > 100) {
      console.warn('严重长任务:', entry.duration, 'ms')
      
      // 上报到分析服务
      reportMetric('long-task', {
        duration: entry.duration,
        url: window.location.href
      })
    }
  })
})

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

长任务分析与优化

// 长任务分析工具
class LongTaskAnalyzer {
  constructor() {
    this.tasks = []
    this.observer = null
  }

  start() {
    this.observer = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        this.tasks.push({
          duration: entry.duration,
          startTime: entry.startTime,
          timestamp: Date.now()
        })
      })
    })

    this.observer.observe({ entryTypes: ['longtask'] })
  }

  stop() {
    if (this.observer) {
      this.observer.disconnect()
    }
  }

  getReport() {
    if (this.tasks.length === 0) {
      return { message: '没有检测到长任务' }
    }

    const totalDuration = this.tasks.reduce((sum, t) => sum + t.duration, 0)
    const avgDuration = totalDuration / this.tasks.length
    const maxDuration = Math.max(...this.tasks.map(t => t.duration))

    return {
      count: this.tasks.length,
      totalDuration: Math.round(totalDuration),
      avgDuration: Math.round(avgDuration),
      maxDuration: Math.round(maxDuration),
      tasks: this.tasks,
      recommendation: this.getRecommendation(avgDuration)
    }
  }

  getRecommendation(avgDuration) {
    if (avgDuration > 100) {
      return '存在严重的主线程阻塞,建议:1) 代码分割 2) Web Worker 3) 优化关键路径'
    }
    if (avgDuration > 50) {
      return '有一些长任务,建议检查关键操作的执行时间'
    }
    return '长任务情况正常'
  }
}

完整性能监控方案

集成监控类

// 完整的性能监控类
class PerformanceMonitor {
  constructor(options = {}) {
    this.options = {
      reportUrl: options.reportUrl || '/api/performance',
      sampleRate: options.sampleRate || 1, // 采样率
      enableLongTask: options.enableLongTask ?? true,
      enableResourceTiming: options.enableResourceTiming ?? true,
      ...options
    }

    this.metrics = {}
    this.observers = []
  }

  init() {
    // 采样控制
    if (Math.random() > this.options.sampleRate) {
      return
    }

    this.observeNavigation()
    this.observePaint()
    this.observeWebVitals()
    
    if (this.options.enableLongTask) {
      this.observeLongTasks()
    }

    // 页面卸载时上报
    window.addEventListener('beforeunload', () => {
      this.report()
    })

    // 页面隐藏时上报(移动端更可靠)
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.report()
      }
    })
  }

  observeNavigation() {
    window.addEventListener('load', () => {
      setTimeout(() => {
        const [nav] = performance.getEntriesByType('navigation')
        if (nav) {
          this.metrics.navigation = {
            dns: nav.domainLookupEnd - nav.domainLookupStart,
            tcp: nav.connectEnd - nav.connectStart,
            ttfb: nav.responseStart - nav.requestStart,
            download: nav.responseEnd - nav.responseStart,
            domReady: nav.domContentLoadedEventEnd - nav.startTime,
            load: nav.loadEventEnd - nav.startTime
          }
        }
      }, 0)
    })
  }

  observePaint() {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        this.metrics[entry.name] = entry.startTime
      })
    })

    observer.observe({ entryTypes: ['paint'] })
    this.observers.push(observer)
  }

  observeWebVitals() {
    // LCP
    this.observeLCP()
    // FID
    this.observeFID()
    // CLS
    this.observeCLS()
  }

  observeLCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      this.metrics.lcp = lastEntry.startTime
    })

    observer.observe({ entryTypes: ['largest-contentful-paint'] })
    this.observers.push(observer)
  }

  observeFID() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      if (entries.length > 0) {
        const entry = entries[0]
        this.metrics.fid = entry.processingStart - entry.startTime
      }
    })

    observer.observe({ entryTypes: ['first-input'] })
    this.observers.push(observer)
  }

  observeCLS() {
    let clsValue = 0
    let sessionValue = 0
    let sessionEntries = []

    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          const firstSessionEntry = sessionEntries[0]
          const lastSessionEntry = sessionEntries[sessionEntries.length - 1]

          if (
            sessionValue &&
            entry.startTime - lastSessionEntry?.startTime < 1000 &&
            entry.startTime - firstSessionEntry?.startTime < 5000
          ) {
            sessionValue += entry.value
            sessionEntries.push(entry)
          } else {
            sessionValue = entry.value
            sessionEntries = [entry]
          }

          if (sessionValue > clsValue) {
            clsValue = sessionValue
          }
        }
      }
      this.metrics.cls = clsValue
    })

    observer.observe({ entryTypes: ['layout-shift'] })
    this.observers.push(observer)
  }

  observeLongTasks() {
    const observer = new PerformanceObserver((list) => {
      const tasks = list.getEntries()
      if (!this.metrics.longTasks) {
        this.metrics.longTasks = []
      }
      tasks.forEach(task => {
        this.metrics.longTasks.push({
          duration: task.duration,
          startTime: task.startTime
        })
      })
    })

    observer.observe({ entryTypes: ['longtask'] })
    this.observers.push(observer)
  }

  getMetrics() {
    return {
      ...this.metrics,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    }
  }

  report() {
    const metrics = this.getMetrics()
    
    // 使用 sendBeacon 确保数据发送
    if (navigator.sendBeacon) {
      navigator.sendBeacon(
        this.options.reportUrl,
        JSON.stringify(metrics)
      )
    } else {
      // 降级方案
      fetch(this.options.reportUrl, {
        method: 'POST',
        body: JSON.stringify(metrics),
        keepalive: true
      }).catch(() => {})
    }
  }

  destroy() {
    this.observers.forEach(observer => observer.disconnect())
    this.observers = []
  }
}

// 使用示例
const monitor = new PerformanceMonitor({
  reportUrl: '/api/performance',
  sampleRate: 0.1 // 10% 采样
})

monitor.init()

最佳实践

性能监控注意事项

// 性能监控最佳实践
const bestPractices = {
  // 1. 使用采样减少开销
  sampling: {
    description: '不需要收集 100% 用户的数据',
    recommendation: '生产环境使用 1-10% 采样率',
    code: 'if (Math.random() < 0.1) { monitor.init() }'
  },

  // 2. 使用 sendBeacon 上报
  reporting: {
    description: 'sendBeacon 不会阻塞页面卸载',
    recommendation: '始终优先使用 sendBeacon',
    fallback: 'fetch with keepalive: true'
  },

  // 3. 在 visibilitychange 时上报
  timing: {
    description: 'beforeunload 在移动端不可靠',
    recommendation: '同时监听 visibilitychange',
    code: 'document.addEventListener("visibilitychange", ...)'
  },

  // 4. 延迟初始化
  initialization: {
    description: '避免影响页面初始加载',
    recommendation: '在 load 事件后或 requestIdleCallback 中初始化',
    code: 'requestIdleCallback(() => monitor.init())'
  },

  // 5. 数据去重
  deduplication: {
    description: '同一页面可能多次上报',
    recommendation: '在服务端进行去重处理'
  }
}

常见问题解答

Q: Performance API 在哪些浏览器中可用? A: 主要 API 在现代浏览器中广泛支持。Navigation Timing Level 2、Resource Timing、User Timing 支持 IE10+。Web Vitals 相关 API 主要支持 Chromium 浏览器。

Q: performance.now() 和 Date.now() 有什么区别? A: performance.now() 返回高精度时间戳(微秒级),从页面导航开始计时,不受系统时间调整影响。Date.now() 返回毫秒级时间戳,会受系统时间变化影响。

Q: 性能数据采集会影响页面性能吗? A: Performance API 本身开销很小。但要注意:1) 不要频繁调用 getEntries();2) 使用 PerformanceObserver 代替轮询;3) 采样上报减少网络请求。

Q: 如何处理 Performance API 不支持的浏览器? A: 使用特性检测:if ('performance' in window && 'getEntriesByType' in performance),不支持时降级或跳过监控。

总结

Performance API 是 Web 性能监控的核心工具。通过合理使用这些 API,可以:

  1. 精确测量页面加载性能 - Navigation Timing
  2. 分析资源加载瓶颈 - Resource Timing
  3. 监控用户体验指标 - Web Vitals APIs
  4. 追踪自定义性能点 - User Timing
  5. 发现主线程阻塞 - Long Tasks API

核心要点:

  • 使用 PerformanceObserver 而非轮询
  • 关注 Core Web Vitals (LCP, FID, CLS)
  • 实现采样和 sendBeacon 上报
  • 建立完整的性能监控体系

参考资源