终局之战:全链路性能体检与监控

19 阅读5分钟

前言

想象一下这个场景:

凌晨3点,我们的手机突然响了,是监控系统的告警:"LCP指标超过4秒,影响约5000用户"。我们迷迷糊糊地打开电脑,登录监控平台,看到这样的数据:

  • 问题发生时间:凌晨2:45
  • 影响范围:移动端用户
  • 相关版本:v2.3.1
  • 关联代码提交:12分钟前有人合并了PR

我们打开那个PR,发现是新加的首页大图没做懒加载。你回滚代码,5分钟后指标恢复正常,然后安心地继续睡觉。这并不是科幻,而是有性能监控体系的团队日常。

为什么需要性能监控?

被动优化 vs 主动监控

被动优化(事后救火)

用户反馈页面卡顿
    ↓ 3小时后
开发开始排查
    ↓ 2小时后
定位到问题
    ↓ 4小时后
发布修复
    ↓ 1天后
同样的问题又出现了

结果:永远在救火,永远有火!

主动监控(事前预防)

监控系统发现性能下降
    ↓ 1分钟内
自动告警到开发
    ↓ 5分钟内
定位到相关代码
    ↓ 10分钟内
回滚或修复
    ↓ 持续
性能指标保持健康

结果:问题发现早于用户,修复快于影响!

核心问题

  1. 如何知道页面现在有多快?
  2. 如何知道它什么时候变慢了?
  3. 如何知道哪里变慢了?
  4. 如何防止它再次变慢?

核心性能指标

加载指标

指标含义目标怎么测
FCP首次内容绘制< 1.8秒第一个像素出现
LCP最大内容绘制< 2.5秒主要内容出现
TTFB首字节时间< 600ms服务器响应时间

加载指标采集

function collectMetrics() {
  // FCP
  const paint = performance.getEntriesByType('paint')
  const fcp = paint.find(e => e.name === 'first-contentful-paint')
  console.log('FCP:', fcp?.startTime)
  
  // LCP
  const lcpObserver = new PerformanceObserver((list) => {
    const last = list.getEntries().pop()
    console.log('LCP:', last?.startTime)
  })
  lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })
}

交互指标

指标含义目标怎么测
FID首次输入延迟< 100ms点击后多久响应
INP交互到下次绘制< 200ms整体交互响应

交互指标采集

function collectInteraction() {
  const fidObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const fid = entry.processingStart - entry.startTime
      console.log('FID:', fid)
    }
  })
  fidObserver.observe({ entryTypes: ['first-input'] })
}

稳定性指标

指标含义目标怎么测
CLS累积布局偏移< 0.1页面是否乱跳

稳定性指标采集

let clsValue = 0

const clsObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value
    }
  }
  console.log('CLS:', clsValue)
})
clsObserver.observe({ entryTypes: ['layout-shift'] })

性能监控搭建

使用官方 web-vitals 库

安装

npm install web-vitals

配置

// 核心指标采集
import { onCLS, onFID, onLCP, onTTFB } from 'web-vitals'

// 发送到监控平台
function sendToAnalytics(metric) {
  fetch('/api/performance', {
    method: 'POST',
    body: JSON.stringify({
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      timestamp: Date.now()
    }),
    keepalive: true  // 页面关闭前也能发送
  })
}

// 注册所有指标
onCLS(sendToAnalytics)
onFID(sendToAnalytics)
onLCP(sendToAnalytics)
onTTFB(sendToAnalytics)

自定义性能埋点

// services/performance.js
class PerformanceMonitor {
  constructor() {
    this.buffer = []
    this.flushInterval = 5000  // 5秒上报一次
    this.startTimer()
  }
  
  // 记录一个时间点
  start(name) {
    this.marks.set(name, performance.now())
  }
  
  // 结束并上报
  end(name) {
    const start = this.marks.get(name)
    if (start) {
      const duration = performance.now() - start
      this.track({
        type: 'timing',
        name,
        duration,
        url: window.location.href
      })
      this.marks.delete(name)
    }
  }
  
  // 测量 API 调用
  async measureApi(apiName, promise) {
    const start = performance.now()
    try {
      const result = await promise
      this.track({
        type: 'api',
        name: apiName,
        duration: performance.now() - start,
        status: 'success'
      })
      return result
    } catch (error) {
      this.track({
        type: 'api',
        name: apiName,
        duration: performance.now() - start,
        status: 'error'
      })
      throw error
    }
  }
  
  // 添加到缓冲
  track(data) {
    this.buffer.push({
      ...data,
      timestamp: Date.now(),
      userAgent: navigator.userAgent
    })
    
    if (this.buffer.length >= 20) {
      this.flush()
    }
  }
  
  // 上报数据
  flush() {
    if (this.buffer.length === 0) return
    
    const data = [...this.buffer]
    this.buffer = []
    
    // 使用 sendBeacon 确保页面关闭时也能发送
    navigator.sendBeacon('/api/performance', JSON.stringify(data))
  }
  
  startTimer() {
    setInterval(() => this.flush(), this.flushInterval)
  }
}

export const perf = new PerformanceMonitor()

在组件中使用

<script setup>
import { perf } from '@/services/performance'
import { onMounted } from 'vue'

onMounted(() => {
  perf.start('OrderList')
  
  // 加载数据
  perf.measureApi('fetchOrders', fetchOrders())
    .then(() => {
      perf.end('OrderList')
    })
})
</script>

告警与预警

设置性能阈值

// config/thresholds.js
export const thresholds = {
  LCP: { good: 2500, bad: 4000 },
  FID: { good: 100, bad: 300 },
  CLS: { good: 0.1, bad: 0.25 },
  API: { good: 500, bad: 1000 },
  pageLoad: { good: 3000, bad: 5000 }
}

告警规则

// services/alerter.js
class PerformanceAlerter {
  constructor() {
    this.rules = [
      {
        name: 'LCP过高',
        metric: 'LCP',
        condition: (v) => v > 4000,
        message: '页面加载超过4秒',
        cooldown: 3600000  // 1小时
      },
      {
        name: 'API响应慢',
        metric: 'api',
        condition: (v) => v > 1000,
        message: '{{name}} 响应慢: {{duration}}ms',
        cooldown: 300000  // 5分钟
      }
    ]
  }
  
  check(metric) {
    const rule = this.rules.find(r => r.metric === metric.type)
    if (rule && rule.condition(metric.value)) {
      this.sendAlert(rule, metric)
    }
  }
  
  sendAlert(rule, metric) {
    console.log(`🚨 [告警] ${rule.name}: ${rule.message}`)
    
    // 发送到钉钉/飞书/企业微信
    fetch('/api/alert', {
      method: 'POST',
      body: JSON.stringify({
        title: rule.name,
        message: rule.message,
        data: metric
      })
    })
  }
}

CI/CD 集成

PR 时自动检查性能

# .github/workflows/performance.yml
name: Performance Check

on:
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Install
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: http://localhost:4173
          budgetPath: ./budget.json
      
      - name: Comment PR
        if: always()
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs')
            const report = JSON.parse(fs.readFileSync('./lighthouse-report.json'))
            const score = report.categories.performance.score * 100
            
            if (score < 90) {
              core.setFailed(`性能分数 ${score} 低于 90 分`)
            }

性能预算配置

// budget.json
{
  "budgets": [
    {
      "path": "/*",
      "resourceSizes": [
        { "resourceType": "script", "budget": 500 },
        { "resourceType": "stylesheet", "budget": 100 },
        { "resourceType": "image", "budget": 300 }
      ],
      "timings": [
        { "metric": "first-contentful-paint", "budget": 2000 },
        { "metric": "largest-contentful-paint", "budget": 2500 },
        { "metric": "cumulative-layout-shift", "budget": 0.1 }
      ]
    }
  ]
}

性能仪表盘

搭建简单看板

// 收集一周的性能数据
class PerformanceDashboard {
  constructor() {
    this.data = {
      LCP: [],
      FCP: [],
      CLS: [],
      apiCalls: new Map()
    }
  }
  
  addMetric(metric) {
    this.data[metric.type].push({
      value: metric.value,
      time: metric.timestamp
    })
    
    // 只保留最近7天
    const weekAgo = Date.now() - 7 * 24 * 3600000
    this.data[metric.type] = this.data[metric.type]
      .filter(d => d.time > weekAgo)
  }
  
  getStats(metric) {
    const values = this.data[metric].map(d => d.value)
    const avg = values.reduce((a, b) => a + b, 0) / values.length
    const p95 = this.percentile(values, 95)
    const p99 = this.percentile(values, 99)
    
    return { avg, p95, p99 }
  }
  
  percentile(values, p) {
    const sorted = [...values].sort((a, b) => a - b)
    const index = Math.ceil(p / 100 * sorted.length) - 1
    return sorted[index]
  }
  
  generateReport() {
    console.log('📊 性能周报')
    console.log('================================')
    console.log(`LCP: 平均 ${this.getStats('LCP').avg}ms, P95 ${this.getStats('LCP').p95}ms`)
    console.log(`FCP: 平均 ${this.getStats('FCP').avg}ms, P95 ${this.getStats('FCP').p95}ms`)
    console.log(`CLS: 平均 ${this.getStats('CLS').avg}`)
    console.log('================================')
  }
}

最佳实践清单

性能设计评审清单

每次新功能开发前,回答这些问题:

  • 路由是否懒加载?
  • 长列表是否用虚拟滚动?
  • 高频输入是否防抖?
  • 是否缓存重复请求?
  • 大数据是否分页?
  • 图片是否压缩?是否用WebP?
  • 字体是否按需加载?
  • 关键路径是否埋点?

性能案例库

记录每次性能优化,用于团队分享:

const cases = [
  {
    title: '订单列表从3秒到1秒',
    problem: '页面加载慢,用户投诉',
    solution: '虚拟滚动 + 按需加载',
    result: 'FCP从3.2s降到1.2s',
    author: '张三',
    date: '2026-01-15'
  },
  {
    title: '导出功能不卡了',
    problem: '导出时页面假死',
    solution: 'Web Worker处理数据',
    result: '页面不卡顿',
    author: '李四',
    date: '2026-02-20'
  }
]

监控体系四要素

1. 采集 - 知道发生了什么

  • 核心指标 (LCP, FID, CLS)
  • 自定义指标 (API, 组件渲染)

2. 分析 - 知道为什么发生

  • 关联代码版本
  • 关联用户群体
  • 关联环境信息

3. 告警 - 第一时间知道

  • 阈值设置
  • 告警渠道
  • 冷却机制

4. 预防 - 防止再次发生

  • CI 自动检查
  • 性能预算
  • 设计评审

结语

性能监控不是终点,而是持续优化的起点。没有监控的性能优化,就像没有仪表的驾驶。我们不知道车有多快,也不知道什么时候会抛锚!

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!