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 逐项优化

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

相关推荐
AlienZHOU2 小时前
从零开始,跟着写一个产品级 Coding Agent
前端
RichardZhiLi2 小时前
大前端全栈实践课程:章节二(前端工程化建设)
前端
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于VUE的环保网站设计为例,包含答辩的问题和答案
前端·javascript·vue.js
ZTrainWilliams2 小时前
swagger-mcp-toolkit 让 AI编辑器 更快“读懂并调用”你的接口
前端·后端·mcp
伊步沁心2 小时前
深入 useEffect:为什么 cleanup 总比 setup 先跑?顺手手写节流防抖 Hook
前端
小J听不清2 小时前
CSS 字体样式全解析:字体类型 / 大小 / 粗细 / 样式
前端·javascript·css·html·css3
500佰3 小时前
pencil on claude 让设计师和程序员少吵架的一种可能
前端
Jane-lan3 小时前
NVM安装以及可能的坑
前端·node·nvm