技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

如何进行 Web 性能监控

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/25-如何进行 web 性能监控
本文目录15 个章节
  1. 一、性能监控不是只测一个加载时间
  2. 二、实验室监控和真实用户监控
  3. 三、核心体验指标
  4. 四、Navigation Timing
  5. 五、用 web-vitals 采集核心指标
  6. 六、使用 PerformanceObserver 做底层诊断
  7. 七、资源加载监控
  8. 八、Server-Timing 和端到端关联
  9. 九、SPA 路由和业务指标
  10. 十、错误、接口和资源失败
  11. 十一、上报 SDK 的设计
  12. 十二、聚合和告警
  13. 十三、诊断到优化的闭环
  14. 总结
  15. 参考资料

如何进行 Web 性能监控

Category(分类): Browser, Performance, Monitoring Status: 已更新

原文只有一个性能监控资料链接。本文保留“通过浏览器 API 采集数据并上报”的主题,补齐现代 Web 性能监控需要的指标、采集、聚合、诊断和隐私边界。

原文资料:如何进行 web 性能监控

一、性能监控不是只测一个加载时间

性能监控要回答:

  1. 用户什么时候看到内容?
  2. 用户点击、输入和滚动是否及时响应?
  3. 页面是否发生布局跳动?
  4. 哪些网络资源、长任务、第三方脚本或服务端阶段拖慢了体验?
  5. 新版本上线后,问题集中在哪些页面、设备、网络和地区?

“首页加载 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 有值时才可计算;
  • DOMContentLoadedload 不是 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 未命中和服务端 db timing 同时升高。

告警要结合流量阈值和持续时间,避免一次冷启动、爬虫或小流量页面触发噪声。

十三、诊断到优化的闭环

现象可能方向进一步数据
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 面板、网络瀑布、代码剖析和服务端日志定位根因。

总结

  1. 性能监控要同时覆盖实验室和真实用户数据;
  2. LCP、INP、CLS 是核心体验指标,FCP、TTFB、长任务和资源 Timing 用于补充;
  3. performance.timing 已过时,应使用 Navigation Timing 和 PerformanceObserver;
  4. SPA 路由、白屏、首屏和业务可用时间需要明确自定义定义;
  5. 使用 web-vitals 采集核心指标,使用底层 API 诊断资源、长任务和布局问题;
  6. Server-Timing、Trace ID 和错误数据可以把浏览器体验与后端根因串起来;
  7. RUM SDK 要批量、采样、脱敏、限流并在页面隐藏时可靠上报;
  8. 使用 p75/p95 和版本、设备、网络维度做聚合和回归,而不是只看平均值。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS