Web Vitals 与前端性能监控实战
一、为什么要做性能监控
- 页面加载超过 3 秒 ,用户流失率增加 53%(Google 数据)
- 性能直接影响 SEO 排名(Core Web Vitals 是 Google 排名信号之一)
- 性能问题不能只靠开发者本地测试,需要真实用户数据(RUM) 来发现线上瓶颈
二、核心概念:Web Vitals
Google 定义了三个核心指标来衡量用户体验:
| 指标 | 全称 | 衡量维度 | 优秀 | 需改进 | 差 |
|---|---|---|---|---|---|
| LCP | Largest Contentful Paint | 加载性能 | ≤ 2.5s | ≤ 4.0s | > 4.0s |
| FID/INP | Interaction to Next Paint | 交互响应 | ≤ 200ms | ≤ 500ms | > 500ms |
| CLS | Cumulative Layout Shift | 视觉稳定性 | ≤ 0.1 | ≤ 0.25 | > 0.25 |
补充指标: FCP(First Contentful Paint)、TTFB(Time to First Byte)也是常用的辅助诊断指标。
三、浏览器提供的两大 API
3.1 Navigation Timing API
获取页面从请求到加载完成的完整时间线:
fetchStart → DNS → TCP → Request → Response → DOM Parse → Load
javascript
const [entry] = performance.getEntriesByType('navigation')
const metrics = {
dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS 查询
tcp: entry.connectEnd - entry.connectStart, // TCP 连接
ttfb: entry.responseStart - entry.requestStart, // 首字节时间
load: entry.loadEventEnd - entry.fetchStart, // 完整加载时间
}
3.2 PerformanceObserver
以观察者模式异步监听各类性能条目,不阻塞主线程:
javascript
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.startTime)
}
})
// 支持的 type:paint、largest-contentful-paint、layout-shift、longtask 等
observer.observe({ type: 'paint', buffered: true })
buffered: true表示拿到观察器创建之前已经产生的条目,防止遗漏。
四、实战:采集 Web Vitals
4.1 FCP --- 首次内容绘制
javascript
function observeFCP(callback) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
callback(Math.round(entry.startTime))
observer.disconnect()
}
}
})
observer.observe({ type: 'paint', buffered: true })
}
// 使用
observeFCP((value) => {
console.log(`FCP: ${value}ms`)
// 上报到你的监控平台
})
4.2 LCP --- 最大内容绘制
LCP 会持续更新,直到用户首次交互或页面隐藏时才确定最终值:
javascript
function observeLCP(callback) {
let lastValue = 0
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
lastValue = entries[entries.length - 1].startTime
})
observer.observe({ type: 'largest-contentful-paint', buffered: true })
const report = () => {
if (lastValue > 0) {
callback(Math.round(lastValue))
}
observer.disconnect()
}
// 用户交互或页面隐藏时取最终值
;['keydown', 'click'].forEach((type) => {
window.addEventListener(type, report, { once: true, capture: true })
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') report()
})
}
为什么不能立即上报? 因为浏览器会不断重新评估"最大内容元素",在页面完全稳定前拿到的值可能不准。
4.3 CLS --- 累积布局偏移
javascript
function observeCLS(callback) {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 排除用户输入引起的布局偏移(如点击展开)
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
})
observer.observe({ type: 'layout-shift', buffered: true })
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
callback(Math.round(clsValue * 1000) / 1000)
observer.disconnect()
}
})
}
CLS 常见元凶:
- 图片/广告未设置宽高
- 动态插入的 DOM 元素
- Web 字体加载导致的文字跳动(FOUT)
五、SPA 路由切换监控
SPA 页面切换不会触发 window.onload,需要手动打点:
javascript
// router.beforeEach
let routeStartTime = 0
export function markRouteStart() {
routeStartTime = performance.now()
}
// router.afterEach
export function reportRouteChange(from, to) {
if (!routeStartTime) return
const duration = Math.round(performance.now() - routeStartTime)
routeStartTime = 0
if (duration <= 0) return
report('route_change', {
from: from.fullPath,
to: to.fullPath,
duration,
})
}
在 Vue Router 中使用:
javascript
router.beforeEach((to, from, next) => {
markRouteStart()
next()
})
router.afterEach((to, from) => {
reportRouteChange(from, to)
})
六、上报策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
navigator.sendBeacon() |
异步、不阻塞页面卸载 | 推荐,页面关闭时也能发送 |
fetch + keepalive |
类似 Beacon,支持自定义 Header | 需要认证的场景 |
new Image().src |
兼容性最好 | 极简场景 / 老浏览器兜底 |
javascript
function report(event, data) {
const payload = JSON.stringify({ event, ...data, url: location.href })
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/report', payload)
} else {
new Image().src = `/api/report?d=${encodeURIComponent(payload)}`
}
}
七、性能优化速查表
| 指标 | 优化手段 |
|---|---|
| LCP | 图片预加载 <link rel="preload">、使用 CDN、SSR/SSG、优化服务端响应时间 |
| FID/INP | 拆分长任务(setTimeout / requestIdleCallback)、减少主线程 JS 执行时间 |
| CLS | 给 img/video 设置明确的 width/height、预留骨架屏空间、font-display: swap |
| FCP | 内联关键 CSS、减少阻塞渲染的资源、开启 gzip/brotli 压缩 |
| TTFB | 使用 CDN、开启 HTTP/2、优化数据库查询、使用缓存 |
八、常见问题 FAQ
Q:为什么本地性能很好,线上却很差?
本地网络快、设备好,无法代表真实用户。建议关注 P75/P90 分位值,而非平均值。
Q:PerformanceObserver 兼容性如何?
主流浏览器(Chrome 52+、Firefox 57+、Safari 14.1+)均支持,使用时做
try/catch兼容即可。
Q:FID 和 INP 有什么区别?
FID 只测量首次 交互延迟,INP 测量所有交互中最差的延迟。Google 已在 2024 年用 INP 替代 FID 作为核心指标。
九、总结
采集层:PerformanceObserver + Navigation Timing
↓
上报层:sendBeacon / fetch keepalive
↓
分析层:P75/P90 分位、趋势图、异常告警
↓
优化层:针对 LCP/INP/CLS 逐项优化
性能监控不是一次性工程,而是持续度量 → 发现问题 → 优化 → 验证效果的循环过程。