Web 性能监控体系:从 RUM 到 Core Web Vitals 实战
引言
性能不是"感觉快不快",而是可量化的工程指标。Google 的 Core Web Vitals 已经成为搜索排名因子之一。本文系统性地讲解 RUM 体系搭建、Core Web Vitals 采集原理、Performance Observer API 实战,以及告警与优化策略。
一、RUM vs Synthetic Monitoring
| 维度 | RUM(Real User Monitoring) | Synthetic(合成监控) |
|---|---|---|
| 数据来源 | 真实用户浏览器 | 固定环境脚本 |
| 网络条件 | 真实分布 | 实验室稳定 |
| 用户基数 | 大量真实样本 | 有限场景 |
| 反映问题 | 真实体验瓶颈 | 可重现的性能回归 |
| 典型工具 | web-vitals、Sentry Performance | Lighthouse CI、WebPageTest |
| 成本 | 低(边缘采集) | 中(机器实例) |
最佳实践:RUM 为主,Synthetic 为辅,互为补充。
二、Core Web Vitals 三大指标
| 指标 | 全称 | 含义 | 良好 | 需改进 | 差 |
|---|---|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容绘制 | ≤2.5s | ≤4.0s | >4.0s |
| INP | Interaction to Next Paint | 交互到下一帧 | ≤200ms | ≤500ms | >500ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | ≤0.1 | ≤0.25 | >0.25 |
⚠️ 2024 年 3 月起,INP 正式替代 FID 成为 Core Web Vital。
辅助指标:TTFB(≤800ms)、FCP(≤1.8s)、Long Task(>50ms 的任务,INP 的根因排查入口)。
三、Performance Observer API 实战
3.1 监听 LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.startTime, lastEntry.element)
})
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true })
3.2 监听 CLS
let clsValue = 0
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) clsValue += entry.value
}
})
clsObserver.observe({ type: 'layout-shift', buffered: true })
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') console.log('Final CLS:', clsValue)
})
3.3 INP 采集
let maxInteraction = 0
const inpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > maxInteraction) maxInteraction = entry.duration
}
})
inpObserver.observe({ type: 'event', buffered: true })
3.4 Long Task 监控
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long Task:', entry.duration, 'ms')
}
})
longTaskObserver.observe({ type: 'longtask', buffered: true })
四、web-vitals 库实战
4.1 基础采集
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'
function sendToAnalytics(metric: any) {
const body = JSON.stringify({
name: metric.name, value: metric.value, rating: metric.rating,
id: metric.id, delta: metric.delta, navigationType: metric.navigationType,
page: location.pathname, sessionId: getSessionId(),
})
if (navigator.sendBeacon) navigator.sendBeacon('/api/vitals', body)
else fetch('/api/vitals', { body, method: 'POST', keepalive: true })
}
onLCP(sendToAnalytics); onINP(sendToAnalytics); onCLS(sendToAnalytics)
onFCP(sendToAnalytics); onTTFB(sendToAnalytics)
4.2 attribution:定位瓶颈
import { onLCP } from 'web-vitals/attribution'
onLCP((metric) => {
console.log('LCP element:', metric.attribution.element)
console.log('LCP TTFB:', metric.attribution.timeToFirstByte)
console.log('LCP render delay:', metric.attribution.elementRenderDelay)
console.log('LCP resource load:', metric.attribution.resourceLoadTime)
})
| 归因阶段 | 含义 | 优化方向 |
|---|---|---|
| TTFB | 服务端响应 | CDN、SSR、edge |
| Resource load | 资源加载 | 预加载、格式优化 |
| Render delay | 渲染阻塞 | 减少 JS、关键 CSS |
五、完整 RUM 采集方案
5.1 性能 SDK 封装
class VitalsMonitor {
private queue: VitalReport[] = []
private readonly batchLimit = 10
private readonly flushInterval = 5000
constructor() {
this.init(); this.scheduleFlush(); this.bindVisibilityChange()
}
private init() {
const handler = (metric: any) => {
this.queue.push({
name: metric.name, value: metric.value, rating: metric.rating,
page: location.pathname, sessionId: this.getSessionId(),
attribution: metric.attribution, timestamp: Date.now(),
})
if (this.queue.length >= this.batchLimit) this.flush()
}
onLCP(handler); onINP(handler); onCLS(handler); onFCP(handler); onTTFB(handler)
}
private flush() {
if (this.queue.length === 0) return
const batch = this.queue.splice(0)
const body = JSON.stringify(batch)
if (navigator.sendBeacon) navigator.sendBeacon('/api/vitals/batch', body)
else fetch('/api/vitals/batch', { method: 'POST', body, keepalive: true })
}
private scheduleFlush() {
window.setInterval(() => this.flush(), this.flushInterval)
}
private bindVisibilityChange() {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') this.flush()
})
}
private getSessionId(): string {
let id = sessionStorage.getItem('vitals_session')
if (!id) { id = crypto.randomUUID(); sessionStorage.setItem('vitals_session', id) }
return id
}
}
5.2 采样策略
| 场景 | 采样率 | 说明 |
|---|---|---|
| 高流量站点 | 1-5% | 日志量可控 |
| 中流量 | 10% | 平衡 |
| 低流量 | 50-100% | 保证样本量 |
| 关键页面 | 100% | 结账、登录 |
5.3 服务端聚合
SELECT
page,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY value) AS p75_lcp,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY value) AS p95_lcp,
COUNT(*) AS sample_count,
AVG(CASE WHEN rating = 'good' THEN 1.0 ELSE 0 END) AS good_rate
FROM vitals
WHERE name = 'LCP' AND timestamp > NOW() - INTERVAL '24 hours'
GROUP BY page
ORDER BY p75_lcp DESC;
Google 用 P75 作为 Web Vitals 评估分位数:即 75% 的用户体验达到良好,才算通过。
六、告警体系
6.1 阈值告警
const alerts = [
{ name: 'LCP_P75_HIGH', query: 'histogram_quantile(0.75, rate(vitals_lcp_bucket[5m]))',
threshold: 4000, for: '10m', severity: 'warning' },
{ name: 'INP_P75_CRITICAL', query: 'histogram_quantile(0.75, rate(vitals_inp_bucket[5m]))',
threshold: 500, for: '5m', severity: 'critical' },
{ name: 'CLS_P75_HIGH', query: 'histogram_quantile(0.75, rate(vitals_cls_bucket[5m]))',
threshold: 0.25, for: '10m', severity: 'warning' },
]
6.2 SLO burn rate 告警
基于历史数据的 SLO burn rate 告警更精准:
1h burn rate > 13.16x AND 5m burn rate > 14.4x→ Page(快速告警)6h burn rate > 1.0x AND 30m burn rate > 3.0x→ Ticket(慢速告警)
七、优化策略对应表
| 指标差 | 根因 | 优化 |
|---|---|---|
| TTFB 高 | 服务端慢 / 无 CDN | Edge SSR、CDN 缓存 |
| LCP 高 | 大图 / 渲染阻塞 | 图片 preload、关键 CSS inline、字体 font-display: swap |
| LCP 高 | 第三方 JS | 延迟加载、Partytown |
| INP 高 | Long Task | 代码分割、requestIdleCallback、Web Worker |
| INP 高 | 大量 reflow | 虚拟列表、content-visibility: auto |
| CLS 高 | 无尺寸媒体 | aspect-ratio、img width/height |
| CLS 高 | 动态注入内容 | 预留空间、min-height |
7.1 LCP 优化:图片 preload
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<link rel="preload" as="image"
imagesrcset="/hero-480.webp 480w, /hero-960.webp 960w" imagesizes="100vw" />
7.2 INP 优化:让出主线程
async function processData(items: Item[]) {
for (const item of items) {
process(item)
if ('scheduler' in window && 'yield' in scheduler) {
await (scheduler as any).yield()
} else {
await new Promise(r => setTimeout(r, 0))
}
}
}
7.3 CLS 优化:预留空间
.media-container {
aspect-ratio: 16 / 9;
background: #f5f5f5;
}
.skeleton {
min-height: var(--expected-height, 200px);
}
.long-list-item {
content-visibility: auto;
contain-intrinsic-size: 0 60px;
}
八、总结
性能监控体系的三层建设:
- 采集层:
web-vitals+PerformanceObserver,覆盖 LCP/INP/CLS/TTFB/FCP,带 attribution 归因。 - 聚合层:Beacon 批量上报 → 时序数据库 → P75/P95 分位数聚合。
- 告警层:基于 SLO burn rate 而非固定阈值,结合 Slack/PagerDuty 通知。
没有 RUM 的性能优化是盲人摸象。先量化、再优化、再量化,形成闭环。
Comments | 0条评论