引言

Web性能直接影响用户体验和业务转化。现代Web性能监控已经从简单的页面加载时间,发展到精细化的用户体验指标体系。本文将深入探讨Web性能监控的完整体系,从指标定义到采集分析,再到优化实践。

一、Core Web Vitals详解

1.1 LCP(最大内容绘制)

定义与标准

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// LCP测量页面主要内容加载完成的时间
// 良好:≤2.5秒
// 需改进:2.5-4秒
// 差:>4秒

// 使用Performance Observer测量LCP
function measureLCP() {
  return new Promise<PerformanceEntry>((resolve) => {
    new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      resolve(lastEntry)
    }).observe({ entryTypes: ['largest-contentful-paint'] })
  })
}

// 使用web-vitals库
import { onLCP } from 'web-vitals'

onLCP((metric) => {
  console.log('LCP:', metric.value)
  // 上报数据到分析平台
  sendToAnalytics({
    metric: 'LCP',
    value: metric.value,
    rating: metric.rating,  // 'good' | 'needs-improvement' | 'poor'
  })
})

LCP优化策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 优化服务器响应时间
// - 使用CDN
// - 缓存静态资源
// - 预连接到源服务器

// 2. 优化资源加载
<link rel="preload" as="image" href="hero-image.jpg">
<link rel="preconnect" href="https://api.example.com">

// 3. 优化关键渲染路径
// 优先加载LCP元素相关资源

// 4. 压缩和优化图片
<img
  src="hero.webp"
  loading="eager"
  width="1200"
  height="600"
  fetchpriority="high"
>

// 5. 避免客户端JavaScript渲染LCP内容
// 使用服务端渲染或静态生成

1.2 INP(交互到下一次绘制)

定义与标准

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// INP测量页面整体响应能力
// 良好:≤200毫秒
// 需改进:200-500毫秒
// 差:>500毫秒

import { onINP } from 'web-vitals'

onINP((metric) => {
  console.log('INP:', metric.value)
  console.log('Target:', metric.entries[0].target)  // 交互元素
  console.log('Type:', metric.entries[0].name)      // 事件类型

  sendToAnalytics({
    metric: 'INP',
    value: metric.value,
    target: metric.entries[0].target,
    eventType: metric.entries[0].name
  })
})

INP优化策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 减少JavaScript执行时间
// 使用代码分割
const heavyModule = await import('./heavy-module')

// 2. 优化长任务
// 使用requestIdleCallback
function scheduleWork(work: () => void) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => work())
  } else {
    setTimeout(() => work(), 0)
  }
}

// 3. 避免轮询
// ❌ 不好的做法
setInterval(() => {
  checkStatus()
}, 1000)

// ✅ 使用事件驱动
document.addEventListener('visibilitychange', checkStatus)

// 4. 使用Web Workers处理计算密集型任务
const worker = new Worker('worker.js')
worker.postMessage({ data: heavyData })
worker.onmessage = (e) => {
  const result = e.data
}

1.3 CLS(累积布局偏移)

定义与标准

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// CLS测量页面视觉稳定性
// 良好:≤0.1
// 需改进:0.1-0.25
// 差:>0.25

import { onCLS } from 'web-vitals'

onCLS((metric) => {
  console.log('CLS:', metric.value)
  console.log('Entries:', metric.entries)

  sendToAnalytics({
    metric: 'CLS',
    value: metric.value,
    entries: metric.entries.map(entry => ({
      value: entry.value,
      sources: entry.sources?.map(source => ({
        node: source.node?.nodeName,
        currentRect: source.currentRect
      }))
    }))
  })
})

CLS优化策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- 1. 为图片和视频预留空间 -->
<img
  src="image.jpg"
  width="800"
  height="600"
  style="aspect-ratio: 800/600; background: #f0f0f0"
>

<!-- 2. 为动态内容预留空间 -->
<div style="min-height: 200px">
  <div id="dynamic-content"></div>
</div>

<!-- 3. 避免在现有内容上方插入内容 -->
<!-- ❌ 会造成布局偏移 -->
<button onClick="insertAd()">插入广告</button>

<!-- ✅ 使用占位符 -->
<div style="min-height: 250px; background: #f0f0f0">
  <div id="ad-placeholder"></div>
</div>

<!-- 4. 使用transform代替改变尺寸的动画 -->
<style>
.bad {
  transition: height 0.3s;  /* ❌ 会触发CLS */
}

.good {
  transition: transform 0.3s;  /* ✅ 不会触发CLS */
  transform: scaleY(0);
  transform-origin: top;
}
</style>

二、性能API详解

2.1 Performance API基础

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 获取性能指标
function getPerformanceMetrics() {
  const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming

  return {
    // DNS查询
    dnsLookup: perfData.domainLookupEnd - perfData.domainLookupStart,

    // TCP连接
    tcpConnection: perfData.connectEnd - perfData.connectStart,

    // 请求响应
    requestTime: perfData.responseEnd - perfData.requestStart,

    // DOM解析
    domParse: perfData.domComplete - perfData.domInteractive,

    // 首次绘制
    firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime,

    // 首次内容绘制
    firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,

    // 完全加载
    pageLoad: perfData.loadEventEnd - perfData.navigationStart,

    // 内存使用(如果支持)
    memory: (performance as any).memory ? {
      usedJSHeapSize: (performance as any).memory.usedJSHeapSize,
      totalJSHeapSize: (performance as any).memory.totalJSHeapSize,
      jsHeapSizeLimit: (performance as any).memory.jsHeapSizeLimit
    } : null
  }
}

2.2 资源加载监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 监控资源加载性能
function observeResources() {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const resource = entry as PerformanceResourceTiming

      console.log({
        name: resource.name,
        duration: resource.duration,
        size: resource.transferSize,
        // 时间分解
        redirect: resource.redirectEnd - resource.redirectStart,
        dns: resource.domainLookupEnd - resource.domainLookupStart,
        tcp: resource.connectEnd - resource.connectStart,
        request: resource.responseStart - resource.requestStart,
        response: resource.responseEnd - resource.responseStart,
      })
    }
  })

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

// 获取慢速资源
function getSlowResources(threshold = 1000) {
  const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]

  return resources
    .filter(r => r.duration > threshold)
    .map(r => ({
      url: r.name,
      duration: Math.round(r.duration),
      size: Math.round(r.transferSize / 1024) + ' KB'
    }))
    .sort((a, b) => b.duration - a.duration)
}

2.3 长任务监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 监控阻塞主线程的长任务(>50ms)
function observeLongTasks() {
  if (!PerformanceObserver.supportedEntryTypes.includes('longtask')) {
    console.log('Long tasks not supported')
    return
  }

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log('Long task detected:', {
        duration: entry.duration,
        startTime: entry.startTime,
        // 检查是哪个容器引起的
        attribution: (entry as any).attribution?.map((a: any) => ({
          containerType: a.containerType,
          containerName: a.containerName,
          containerSrc: a.containerSrc
        }))
      })
    }
  })

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

2.4 首次输入延迟(FID)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 测量用户首次交互到浏览器响应的时间
function measureFID() {
  return new Promise<number>((resolve) => {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!(entry as any).processingStart) continue

        const fid = (entry as any).processingStart - entry.startTime
        observer.disconnect()
        resolve(fid)
      }
    })

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

    // 如果5秒后没有交互,返回null
    setTimeout(() => {
      observer.disconnect()
      resolve(null)
    }, 5000)
  })
}

三、性能数据采集

3.1 前端埋点系统

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
class PerformanceTracker {
  private metrics: Map<string, number[]> = new Map()
  private config: {
    sampleRate: number
    endpoint: string
    batchSize: number
  }

  constructor(config: { sampleRate: number; endpoint: string; batchSize?: number }) {
    this.config = {
      ...config,
      batchSize: config.batchSize || 10
    }

    this.initObservers()
  }

  private initObservers() {
    // Core Web Vitals
    this.trackLCP()
    this.trackINP()
    this.trackCLS()

    // 其他指标
    this.trackFCP()
    this.trackTTI()
    this.trackResources()
    }

  private trackLCP() {
    // 使用web-vitals库
    import('web-vitals').then(({ onLCP }) => {
      onLCP((metric) => {
        this.record('LCP', metric.value)
      })
    })
  }

  private trackCLS() {
    import('web-vitals').then(({ onCLS }) => {
      onCLS((metric) => {
        this.record('CLS', metric.value)
      })
    })
  }

  private trackINP() {
    import('web-vitals').then(({ onINP }) => {
      onINP((metric) => {
        this.record('INP', metric.value)
      })
    })
  }

  private trackFCP() {
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
          this.record('FCP', entry.startTime)
        }
      }
    }).observe({ entryTypes: ['paint'] })
  }

  private trackTTI() {
    // 使用tti-polyfill
    import('tti-polyfill').then(({ getTTI }) => {
      getTTI().then((tti) => {
        this.record('TTI', tti)
      })
    })
  }

  private trackResources() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const resource = entry as PerformanceResourceTiming
        this.record(`resource_${this.getResourceType(resource.name)}`, resource.duration)
      }
    })
    observer.observe({ entryTypes: ['resource'] })
  }

  private getResourceType(url: string): string {
    if (url.match(/\.(js|jsx)$/)) return 'script'
    if (url.match(/\.(css)$/)) return 'stylesheet'
    if (url.match(/\.(png|jpg|jpeg|webp|gif|svg)$/)) return 'image'
    if (url.match(/\.(woff|woff2|ttf|eot)$/)) return 'font'
    return 'other'
  }

  private record(metric: string, value: number) {
    if (!this.metrics.has(metric)) {
      this.metrics.set(metric, [])
    }

    this.metrics.get(metric)!.push(value)

    // 达到批量大小时发送
    if (this.metrics.get(metric)!.length >= this.config.batchSize) {
      this.flush()
    }
  }

  private flush() {
    if (this.metrics.size === 0) return

    const data = Object.fromEntries(
      Array.from(this.metrics.entries()).map(([key, values]) => [
        key,
        {
          count: values.length,
          avg: values.reduce((a, b) => a + b, 0) / values.length,
          min: Math.min(...values),
          max: Math.max(...values),
          p50: this.percentile(values, 50),
          p75: this.percentile(values, 75),
          p95: this.percentile(values, 95)
        }
      ])
    )

    fetch(this.config.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        url: location.href,
        userAgent: navigator.userAgent,
        timestamp: Date.now(),
        metrics: data
      }),
      keepalive: true
    })

    this.metrics.clear()
  }

  private percentile(values: number[], p: number): number {
    const sorted = values.slice().sort((a, b) => a - b)
    const index = Math.ceil((p / 100) * sorted.length) - 1
    return sorted[index]
  }
}

// 使用
const tracker = new PerformanceTracker({
  sampleRate: 0.1,  // 10%采样率
  endpoint: 'https://analytics.example.com/performance',
  batchSize: 20
})

3.2 用户行为分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class UserBehaviorTracker {
  private events: Array<any> = []

  init() {
    // 点击事件
    document.addEventListener('click', (e) => {
      this.track('click', {
        target: this.getTargetSelector(e.target as HTMLElement),
        x: e.clientX,
        y: e.clientY
      })
    }, true)

    // 滚动事件
    let scrollTimeout: NodeJS.Timeout
    document.addEventListener('scroll', () => {
      clearTimeout(scrollTimeout)
      scrollTimeout = setTimeout(() => {
        this.track('scroll', {
          scrollDepth: this.getScrollDepth()
        })
      }, 100)
    }, true)

    // 页面可见性变化
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.track('pagehide', {
          timeOnPage: performance.now()
        })
        this.flush()
      }
    })
  }

  private track(type: string, data: any) {
    this.events.push({
      type,
      data,
      timestamp: Date.now(),
      url: location.href
    })

    if (this.events.length > 50) {
      this.flush()
    }
  }

  private getTargetSelector(element: HTMLElement): string {
    if (element.id) return `#${element.id}`
    if (element.className) return `.${element.className.split(' ')[0]}`
    return element.tagName.toLowerCase()
  }

  private getScrollDepth(): number {
    const scrollTop = window.scrollY
    const docHeight = document.documentElement.scrollHeight - window.innerHeight
    return Math.round((scrollTop / docHeight) * 100)
  }

  private flush() {
    if (this.events.length === 0) return

    // 使用sendBeacon确保数据发送
    navigator.sendBeacon('/api/analytics', JSON.stringify(this.events))
    this.events = []
  }
}

四、性能诊断与优化

4.1 性能瓶颈分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 性能诊断报告
class PerformanceDiagnostics {
  async analyze() {
    const report = {
      metrics: await this.getMetrics(),
      issues: this.detectIssues(),
      recommendations: this.getRecommendations()
    }

    return report
  }

  private async getMetrics() {
    const webVitals = await import('web-vitals')

    return Promise.all([
      this.getLCP(webVitals),
      this.getCLS(webVitals),
      this.getINP(webVitals),
      this.getFCP(),
      this.getTTI()
    ])
  }

  private detectIssues() {
    const issues: string[] = []
    const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]

    // 检查大资源
    const largeResources = resources.filter(r => r.transferSize > 500_000)
    if (largeResources.length > 0) {
      issues.push(`Found ${largeResources.length} resources larger than 500KB`)
    }

    // 检查慢资源
    const slowResources = resources.filter(r => r.duration > 2000)
    if (slowResources.length > 0) {
      issues.push(`Found ${slowResources.length} resources taking >2s to load`)
    }

    // 检查未压缩资源
    const uncompressed = resources.filter(r => {
      const encoding = r.transferSize < r.encodedBodySize ? 'gzip' : 'none'
      return encoding === 'none' && r.transferSize > 10_000
    })
    if (uncompressed.length > 0) {
      issues.push(`${uncompressed.length} resources not compressed`)
    }

    return issues
  }

  private getRecommendations() {
    return [
      'Enable Brotli compression',
      'Use image optimization (WebP, responsive images)',
      'Implement code splitting',
      'Add resource hints (preconnect, preload)',
      'Optimize critical rendering path',
      'Remove unused CSS/JS',
      'Implement service worker caching'
    ]
  }
}

4.2 性能优化工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 图片优化工具
class ImageOptimizer {
  // 检测未优化的图片
  detectUnoptimizedImages() {
    const images = document.querySelectorAll('img')
    const issues: any[] = []

    images.forEach(img => {
      // 检查缺少尺寸属性
      if (!img.width || !img.height) {
        issues.push({
          element: img,
          issue: 'Missing width/height attributes',
          severity: 'high'
        })
      }

      // 检查是否使用现代格式
      const src = img.currentSrc || img.src
      if (src.match(/\.(jpg|png)$/i)) {
        issues.push({
          element: img,
          issue: 'Using old image format (use WebP)',
          severity: 'medium'
        })
      }

      // 检查是否可以懒加载
      if (!img.loading && !this.isInViewport(img)) {
        issues.push({
          element: img,
          issue: 'Should use lazy loading',
          severity: 'low'
        })
      }
    })

    return issues
  }

  private isInViewport(element: Element): boolean {
    const rect = element.getBoundingClientRect()
    return rect.top < window.innerHeight && rect.bottom > 0
  }
}

// 字体优化工具
class FontOptimizer {
  optimize() {
    // 字体显示策略
    const style = document.createElement('style')
    style.textContent = `
      @font-face {
        font-display: swap;
      }
    `
    document.head.appendChild(style)

    // 预加载关键字体
    this.preloadFont('/fonts/main.woff2', 'woff2')
  }

  private preloadFont(url: string, format: string) {
    const link = document.createElement('link')
    link.rel = 'preload'
    link.as = 'font'
    link.href = url
    link.type = `font/${format}`
    link.crossOrigin = 'anonymous'
    document.head.appendChild(link)
  }
}

总结

Web性能监控是一个持续优化的过程:

  1. Core Web Vitals - 聚焦用户体验核心指标
  2. 性能API - 充分利用浏览器提供的监控能力
  3. 数据采集 - 建立完善的性能数据收集体系
  4. 诊断分析 - 准确定位性能瓶颈
  5. 持续优化 - 基于数据驱动持续改进

良好的性能监控体系能帮助团队及时发现问题、定位瓶颈,并验证优化效果。

相关工具推荐