Web Vitals 与前端性能监控实战

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

获取页面从请求到加载完成的完整时间线:

复制代码
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 逐项优化

性能监控不是一次性工程,而是持续度量 → 发现问题 → 优化 → 验证效果的循环过程。

相关推荐
踩着两条虫16 分钟前
可视化 vs 终端 vs 云端:VTJ.PRO、Claude Code、Codex 三强横评
前端·vue.js·人工智能·低代码·架构
kyriewen18 分钟前
Godot 全面封杀 Vibe Coding,作为每天用 AI 写代码的人,我反而觉得这是件好事
前端·ai编程·claude
前进的搬砖er30 分钟前
4.three.js的几何体、纹理、光源、材质、uv属性 讲义
前端
前端毕业班44 分钟前
uni-app 小程序支持 teleport 了
前端·javascript·vue.js
东风破_1 小时前
LLM 为什么记不住你?无状态、messages 和上下文管理
javascript·人工智能
梨子同志1 小时前
qiankun
前端
梨子同志1 小时前
Nginx
前端
梨子同志2 小时前
NestJS
前端
梨子同志2 小时前
Next.js
前端
谭光志2 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程