前言
想象一下这个场景:
凌晨3点,我们的手机突然响了,是监控系统的告警:"LCP指标超过4秒,影响约5000用户"。我们迷迷糊糊地打开电脑,登录监控平台,看到这样的数据:
- 问题发生时间:凌晨2:45
- 影响范围:移动端用户
- 相关版本:v2.3.1
- 关联代码提交:12分钟前有人合并了PR
我们打开那个PR,发现是新加的首页大图没做懒加载。你回滚代码,5分钟后指标恢复正常,然后安心地继续睡觉。这并不是科幻,而是有性能监控体系的团队日常。
为什么需要性能监控?
被动优化 vs 主动监控
被动优化(事后救火)
用户反馈页面卡顿
↓ 3小时后
开发开始排查
↓ 2小时后
定位到问题
↓ 4小时后
发布修复
↓ 1天后
同样的问题又出现了
结果:永远在救火,永远有火!
主动监控(事前预防)
监控系统发现性能下降
↓ 1分钟内
自动告警到开发
↓ 5分钟内
定位到相关代码
↓ 10分钟内
回滚或修复
↓ 持续
性能指标保持健康
结果:问题发现早于用户,修复快于影响!
核心问题
- 如何知道页面现在有多快?
- 如何知道它什么时候变慢了?
- 如何知道哪里变慢了?
- 如何防止它再次变慢?
核心性能指标
加载指标
| 指标 | 含义 | 目标 | 怎么测 |
|---|---|---|---|
| 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 自动检查
- 性能预算
- 设计评审
结语
性能监控不是终点,而是持续优化的起点。没有监控的性能优化,就像没有仪表的驾驶。我们不知道车有多快,也不知道什么时候会抛锚!
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!