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

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

相关推荐
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
从前慢丶1 天前
前端交互规范(Web 端)
前端
@yanyu6661 天前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot
CHU7290351 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing1 天前
Page-agent MCP结构
前端·人工智能
王霸天1 天前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化
小领航1 天前
用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件
前端·github
@大迁世界1 天前
2026年React大洗牌:React Hooks 将迎来重大升级
前端·javascript·react.js·前端框架·ecmascript
PieroPc1 天前
一个功能强大的 Web 端标签设计和打印工具,支持服务器端直接打印到局域网打印机。Fastapi + html
前端·html·fastapi