如何进行 Web 性能监控
Category(分类): Browser, Performance, Monitoring Status: 已更新
原文只有一个性能监控资料链接。本文保留“通过浏览器 API 采集数据并上报”的主题,补齐现代 Web 性能监控需要的指标、采集、聚合、诊断和隐私边界。
原文资料:如何进行 web 性能监控
一、性能监控不是只测一个加载时间
性能监控要回答:
- 用户什么时候看到内容?
- 用户点击、输入和滚动是否及时响应?
- 页面是否发生布局跳动?
- 哪些网络资源、长任务、第三方脚本或服务端阶段拖慢了体验?
- 新版本上线后,问题集中在哪些页面、设备、网络和地区?
“首页加载 2 秒”这种单一数字无法覆盖真实体验。一个页面可以很快显示骨架屏,却很晚展示主要内容;也可以 LCP 很好,但点击按钮时被 1 秒的 JavaScript 长任务卡住。
二、实验室监控和真实用户监控
1. 实验室数据(Lab)
Lighthouse、WebPageTest、DevTools Performance 和 CI 性能预算使用固定设备、浏览器、CPU 和网络条件,适合:
- 重复测试和版本对比;
- 定位渲染阻塞资源、脚本执行和布局问题;
- 检查构建产物大小、请求瀑布和第三方脚本;
- 在合并代码前发现回归。
2. 真实用户监控(RUM)
RUM 在真实用户浏览器中采集 Performance API、Web Vitals、错误和网络信息,适合观察:
- 低端手机、弱网、特定运营商和地区;
- 真实用户的 p50、p75、p90/p95;
- 发布、A/B 实验和 CDN 变化带来的影响;
- 性能与转化、留存、错误率的关系。
两者应该组合使用:实验室数据负责可重复诊断,RUM 数据负责验证线上真实收益。不要把实验室分数直接当作所有用户的体验。
三、核心体验指标
1. LCP:主要内容加载
LCP(Largest Contentful Paint)表示视口内最大的文本、图片或视频海报等内容完成绘制的时间。常用 p75 参考线:
- ≤ 2.5 秒:良好;
- 2.5–4 秒:需要改进;
4 秒:较差。
LCP 可能随着候选元素变化,在页面隐藏、用户交互或导航结束时取最终候选值。它不是“最后一张首屏图片的 onload”,也不是 load 事件。
2. INP:交互响应
INP(Interaction to Next Paint)观察页面生命周期内交互到下一次视觉更新的延迟,覆盖点击、键盘和指针交互。常用 p75 参考线:
- ≤ 200 毫秒:良好;
- 200–500 毫秒:需要改进;
500 毫秒:较差。
不要把某个 click 回调的执行时间简单当作 INP。诊断时需要结合事件处理、主线程长任务、样式计算、布局和绘制。
3. CLS:布局稳定性
CLS(Cumulative Layout Shift)衡量没有用户输入原因的意外布局移动。常用 p75 参考线:
- ≤ 0.1:良好;
- 0.1–0.25:需要改进;
0.25:较差。
图片没有宽高、异步插入广告、字体替换和顶部插入提示都可能导致 CLS。为媒体预留尺寸、使用 aspect-ratio、避免在已有内容上方插入内容,可以降低布局跳动。
4. FCP、TTFB 和自定义指标
- FCP:首次绘制文本、图片或非白色 canvas 内容;
- TTFB:从导航开始到收到响应第一个字节的时间,受到网络、TLS、CDN、服务端和缓存影响;
- DOMContentLoaded:HTML 解析及相关脚本阶段完成,不代表页面可用;
load:文档依赖资源完成,不代表主要内容和交互完成;- “白屏时间”“首屏时间”“业务模块可见时间”可以保留,但必须定义测量起点、结束条件和失败行为。
TTI 已不再是当前 Core Web Vitals,不能用它替代 INP。历史报告中的 TTI 可以保留用于对比,但新监控应优先使用 LCP、INP、CLS。
四、Navigation Timing
现代代码不要依赖已经过时的 performance.timing:
const navigation = performance.getEntriesByType('navigation')[0]
if (navigation) {
const metrics = {
navigationType: navigation.type,
redirect: navigation.redirectEnd - navigation.redirectStart,
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
tls: navigation.secureConnectionStart > 0
? navigation.connectEnd - navigation.secureConnectionStart
: 0,
request: navigation.responseStart - navigation.requestStart,
response: navigation.responseEnd - navigation.responseStart,
domInteractive: navigation.domInteractive,
domContentLoaded: navigation.domContentLoadedEventEnd,
load: navigation.loadEventEnd
}
console.table(metrics)
}
需要谨慎解释这些字段:
- 命中 HTTP 缓存、Service Worker、预连接或复用连接时,网络阶段可能为 0 或不代表一次真实网络往返;
- HTTPS 的 TLS 时间只有在
secureConnectionStart有值时才可计算; DOMContentLoaded和load不是 Web Vitals;- 跨源资源的详细时间通常需要服务端配置
Timing-Allow-Origin; - 导航入口只覆盖整页导航,不覆盖 SPA 内部路由。
五、用 web-vitals 采集核心指标
生产环境推荐使用维护良好的 web-vitals 库处理浏览器差异、指标生命周期和归因:
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals'
function reportMetric(metric) {
report({
type: 'web-vital',
name: metric.name,
value: metric.value,
id: metric.id,
rating: metric.rating,
navigationType: metric.navigationType
})
}
onCLS(reportMetric)
onINP(reportMetric)
onLCP(reportMetric)
onFCP(reportMetric)
onTTFB(reportMetric)
其中 TTFB 和 FCP 是补充指标,LCP、INP、CLS 是当前核心体验指标。不要只发送 value,最好同时记录指标 ID、评级、构建版本和页面路由,便于聚合和追踪。
六、使用 PerformanceObserver 做底层诊断
1. Paint 和 LCP
if ('PerformanceObserver' in window) {
const paintObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
report({ type: 'paint', name: entry.name, value: entry.startTime })
}
}
})
paintObserver.observe({ type: 'paint', buffered: true })
let lastLcp
const lcpObserver = new PerformanceObserver(list => {
lastLcp = list.getEntries().at(-1)
})
lcpObserver.observe({
type: 'largest-contentful-paint',
buffered: true
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'hidden' || !lastLcp) return
report({
type: 'lcp-diagnostic',
value: lastLcp.startTime,
element: lastLcp.element?.tagName,
url: lastLcp.url
})
lcpObserver.disconnect()
}, { once: true })
}
不要把元素的完整文本、完整 URL 查询参数和用户输入发送到监控平台,应做脱敏和长度限制。
2. 布局位移
let clsValue = 0
let sessionValue = 0
let sessionStart = 0
let previousShift = 0
const clsObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue
const gap = entry.startTime - previousShift
if (
sessionStart &&
(gap > 1000 || entry.startTime - sessionStart > 5000)
) {
sessionValue = 0
sessionStart = entry.startTime
}
if (!sessionStart) sessionStart = entry.startTime
sessionValue += entry.value
clsValue = Math.max(clsValue, sessionValue)
previousShift = entry.startTime
}
})
clsObserver.observe({ type: 'layout-shift', buffered: true })
这是诊断示例,不建议在生产 SDK 中自行复制全部 Web Vitals 算法;优先使用 web-vitals。
3. 长任务和交互
if ('PerformanceObserver' in window) {
const longTaskObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
report({
type: 'long-task',
duration: entry.duration,
startTime: entry.startTime
})
}
})
longTaskObserver.observe({ type: 'longtask', buffered: true })
const eventObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (!entry.interactionId) continue
report({
type: 'interaction-diagnostic',
name: entry.name,
duration: entry.duration,
interactionId: entry.interactionId
})
}
})
eventObserver.observe({
type: 'event',
durationThreshold: 16,
buffered: true
})
}
长任务超过 50ms 时可能阻塞输入和渲染。定位问题时可继续检查调用栈、脚本来源和第三方代码。事件 Timing 是诊断信息,不应把每条事件都当作最终 INP。
七、资源加载监控
const resources = performance.getEntriesByType('resource')
for (const entry of resources) {
report({
type: 'resource',
name: normalizeUrl(entry.name),
initiatorType: entry.initiatorType,
duration: entry.duration,
ttfb: entry.responseStart - entry.requestStart,
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize
})
}
重点关注:
- 阻塞渲染的 CSS、字体和脚本;
- LCP 图片是否被 lazy-load、是否尺寸过大;
- JS 解析/执行成本和重复依赖;
transferSize为 0 是否来自缓存、Service Worker 或跨源计时限制;- 第三方脚本的加载、执行和错误;
- 请求瀑布中是否存在不必要的串行依赖。
跨源资源想暴露详细 Resource Timing,通常需要响应头:
Timing-Allow-Origin: https://app.example.com
监控 URL 时应移除 Token、邮箱、手机号和其他敏感查询参数:
function normalizeUrl(raw) {
const url = new URL(raw)
url.search = ''
return `${url.origin}${url.pathname}`
}
八、Server-Timing 和端到端关联
后端可以通过 Server-Timing 把服务端阶段传给浏览器:
Server-Timing: cache;desc="HIT", db;dur=18, render;dur=6
前端:
const navigation = performance.getEntriesByType('navigation')[0]
for (const timing of navigation?.serverTiming ?? []) {
console.log({
name: timing.name,
duration: timing.duration,
description: timing.description
})
}
更完整的系统还会为一次导航生成 Request ID 或 Trace ID:
用户页面 -> CDN -> API 网关 -> 服务 -> 数据库
| | | |
+-----------同一个 trace/request id-----------+
这样可以把前端 TTFB、API duration、CDN 命中和数据库慢查询关联起来。不要把完整用户身份、Cookie 或 Authorization 头写进 Server-Timing。
九、SPA 路由和业务指标
整页导航的 Navigation Timing 不会在 SPA 路由切换时重新产生。应在路由开始和关键内容渲染完成处自定义测量:
router.beforeEach(() => {
performance.mark('spa-route-start')
})
router.afterEach(async to => {
await nextTick()
await new Promise(requestAnimationFrame)
performance.mark('spa-route-ready')
performance.measure(
`spa-route:${to.name}`,
'spa-route-start',
'spa-route-ready'
)
const measure = performance
.getEntriesByName(`spa-route:${to.name}`)
.at(-1)
report({
type: 'spa-route',
route: String(to.name),
duration: measure?.duration
})
performance.clearMarks('spa-route-start')
performance.clearMarks('spa-route-ready')
performance.clearMeasures(`spa-route:${to.name}`)
})
这是“路由可用时间”,不是 LCP。业务还可以定义:
- 搜索结果首条数据可见;
- 编辑器可输入;
- 主按钮可点击;
- 图表首屏完成;
- 离线数据恢复完成。
每个自定义指标必须写清起止点,否则不同团队会用同一个名称表示不同事情。
十、错误、接口和资源失败
性能监控最好和错误监控关联:
window.addEventListener('error', event => {
if (event.target instanceof HTMLImageElement) {
report({
type: 'resource-error',
resource: normalizeUrl(event.target.src)
})
return
}
report({
type: 'runtime-error',
message: event.message,
source: event.filename,
line: event.lineno,
column: event.colno
})
}, true)
window.addEventListener('unhandledrejection', event => {
report({
type: 'unhandled-rejection',
reason: String(event.reason).slice(0, 500)
})
})
Fetch/XHR 监控可以记录请求耗时、状态码、超时、重试和接口名称,但要避免:
- 记录完整请求体和响应体;
- 把密码、Token 和个人信息发送到日志;
- monkey patch 所有 API 导致 SDK 本身改变页面行为;
- 对每条请求都同步上报。
十一、上报 SDK 的设计
1. 批量、采样和页面离开
const queue = []
let flushTimer
function report(event) {
queue.push({
...event,
page: location.pathname,
build: window.__BUILD_ID__,
viewport: `${innerWidth}x${innerHeight}`,
at: Date.now()
})
if (queue.length >= 10) flush()
else {
clearTimeout(flushTimer)
flushTimer = setTimeout(flush, 3000)
}
}
function flush() {
if (!queue.length) return
const payload = JSON.stringify(queue.splice(0))
if (navigator.sendBeacon) {
navigator.sendBeacon(
'/rum',
new Blob([payload], { type: 'application/json' })
)
return
}
fetch('/rum', {
method: 'POST',
body: payload,
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
keepalive: true
}).catch(() => {})
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush()
}, { once: true })
2. 采样策略
- Web Vitals 可以较高采样率采集,长任务和资源明细可以低采样率;
- 错误、慢请求和异常指标可以动态提高采样;
- 对同一用户或同一会话保持稳定采样,便于关联页面和错误;
- 新版本、低端设备、弱网和实验组可以提高采样率;
- 服务端限制请求频率、大小和来源,防止 RUM 接口被滥用。
3. 数据维度
建议保留:
- 页面模板/路由;
- 构建版本和实验分组;
- 指标名、数值、评级和指标 ID;
- 设备类别、浏览器版本、网络类型和地区的粗粒度信息;
- 是否冷启动、缓存命中或 SPA 路由;
- Trace ID/Request ID(不可反推出敏感身份)。
不要默认采集完整 URL、DOM 文本、用户输入、Cookie、Token、IP 明文和指纹化设备信息。性能监控也要遵循最小化、脱敏、访问控制和保留周期要求。
十二、聚合和告警
平均值容易掩盖低端设备和弱网用户。至少观察:
p50:典型用户
p75:Core Web Vitals 常用体验分位点
p90/p95:长尾问题
错误率:页面/接口是否可用
告警示例:
- 某构建版本 LCP p75 连续 15 分钟超过基线;
- INP p75 在低端 Android 上显著回归;
- CLS p75 因某个模板增加而升高;
- 资源 404、JS chunk load error 或 API 超时增加;
- TTFB、CDN 未命中和服务端
dbtiming 同时升高。
告警要结合流量阈值和持续时间,避免一次冷启动、爬虫或小流量页面触发噪声。
十三、诊断到优化的闭环
| 现象 | 可能方向 | 进一步数据 |
|---|---|---|
| LCP 变慢 | TTFB、LCP 图片、阻塞 CSS、字体、服务端数据 | Navigation Timing、Resource Timing、Server-Timing |
| INP 变差 | 长任务、事件处理、同步布局、第三方脚本 | Event Timing、Long Tasks、调用栈 |
| CLS 变差 | 图片尺寸、广告、字体、异步插入 | LayoutShift 来源元素 |
| FCP 变慢 | HTML、CSS、首屏脚本、网络 | Paint、CSS/JS waterfall |
| 只有某地区变慢 | CDN、DNS、运营商、回源 | region、CDN 命中和 DNS |
| 只有新版本变慢 | 构建产物、实验代码、第三方变更 | buildId、资源大小、A/B 分组 |
监控指标只能告诉我们“哪里变差”,最终还需要 Performance 面板、网络瀑布、代码剖析和服务端日志定位根因。
总结
- 性能监控要同时覆盖实验室和真实用户数据;
- LCP、INP、CLS 是核心体验指标,FCP、TTFB、长任务和资源 Timing 用于补充;
performance.timing已过时,应使用 Navigation Timing 和 PerformanceObserver;- SPA 路由、白屏、首屏和业务可用时间需要明确自定义定义;
- 使用
web-vitals采集核心指标,使用底层 API 诊断资源、长任务和布局问题; Server-Timing、Trace ID 和错误数据可以把浏览器体验与后端根因串起来;- RUM SDK 要批量、采样、脱敏、限流并在页面隐藏时可靠上报;
- 使用 p75/p95 和版本、设备、网络维度做聚合和回归,而不是只看平均值。