🎯 Web 性能 API 集合:Performance Observer 的 5 个冷门妙用

48 阅读2分钟

性能监控|浏览器 API | 你以为 Performance API 只是 performance.now()PerformanceObserver 能在不阻塞主线程的前提下监听到 LCP、FMP、长任务、内存泄漏……甚至能捕获「谁在偷偷做耗时的同步操作」。

问题场景

线上项目偶尔出现卡顿,但在 DevTools 里抓不到复现。打点日志也看不出端倪——因为你只能看到结果(页面慢了),看不到原因(谁占用了主线程)。

// 传统的打点方式
const start = performance.now()
doHeavyWork()
console.log(`耗时:${performance.now() - start}ms`)

这种方式有两个致命缺陷:

  1. 侵入性强——要改造业务代码
  2. 抓不到偶发问题——你不知道什么时候该打点

解决方案:PerformanceObserver

PerformanceObserver 是一个观察者模式的性能 API,可以订阅多种性能事件,在事件发生时异步回调,零侵入。

1️⃣ 捕获所有长任务(Long Tasks)

哪些函数霸占了主线程超过 50ms?直接抓:

const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    console.warn(`⚠️ 长任务 ${entry.duration.toFixed(0)}ms`, {
      name: entry.name,
      startTime: entry.startTime,
      duration: entry.duration,
      // attribution 包含具体函数信息(Chrome)
      culprit: entry.attribution?.[0]?.toJSON()
    })
  }
})

observer.observe({ type: 'longtask', buffered: true })

执行结果示例:

⚠️ 长任务 312ms { name: "self", startTime: 4820.3, duration: 312.1, 
  culprit: { containerName: "window", containerId: "", containerSrc: "" } }

💡 配合 attribution 能定位到是哪个 iframe 或 Worker 触发的长任务。

2️⃣ 监控 LCP(最大内容绘制)

LCP 是 Core Web Vitals 核心指标,用 PerformanceObserver 可以精确知道哪个元素是 LCP 元素:

const lcpObserver = new PerformanceObserver(list => {
  const entries = list.getEntries()
  const lastEntry = entries[entries.length - 1]
  console.log(`🎨 LCP 元素:${lastEntry.element || lastEntry.url}`, {
    renderTime: lastEntry.renderTime,
    loadTime: lastEntry.loadTime,
    size: lastEntry.size,
    id: lastEntry.id,
    url: lastEntry.url
  })
})

lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true })

实战场景: 发现 LCP 元素是一张 background-image,排查发现图片没有 preload,加上 <link rel="preload"> 后 LCP 从 4.2s 降到 1.8s。

3️⃣ 自动追踪资源加载时序

想知道某个 API 请求为什么慢?不需要改 fetch 代码:

const resourceObserver = new PerformanceObserver(list => {
  list.getEntries().forEach(entry => {
    if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
      console.log(`🌐 ${entry.name}`, {
        dns: `${entry.domainLookupEnd - entry.domainLookupStart}ms`,
        tcp: `${entry.connectEnd - entry.connectStart}ms`,
        tls: `${entry.secureConnectionStart ? entry.connectEnd - entry.secureConnectionStart : 0}ms`,
        ttfb: `${entry.responseStart - entry.requestStart}ms`,
        download: `${entry.responseEnd - entry.responseStart}ms`,
        total: `${entry.duration}ms`
      })
    }
  })
})

resourceObserver.observe({ type: 'resource', buffered: true })

执行结果示例:

🌐 https://api.example.com/users
  { dns: "2ms", tcp: "15ms", tls: "28ms", ttfb: "320ms", download: "45ms", total: "410ms" }

一眼看出瓶颈在 TTFB(服务端响应慢),不是网络问题。

4️⃣ 监控 First Paint / First Contentful Paint

const paintObserver = new PerformanceObserver(list => {
  list.getEntries().forEach(entry => {
    console.log(`🎨 ${entry.name}: ${entry.startTime}ms`)
  })
})

paintObserver.observe({ type: 'paint', buffered: true })
// 🎨 first-paint: 435ms
// 🎨 first-contentful-paint: 435ms

5️⃣ 捕获 Layout Shift(CLS 指标)

const clsObserver = new PerformanceObserver(list => {
  let cls = 0
  list.getEntries().forEach(entry => {
    if (!entry.hadRecentInput) {
      cls += entry.value
      console.warn(`💥 Layout Shift: ${entry.value.toFixed(3)}`, {
        source: entry.sources?.[0]?.node || 'unknown'
      })
    }
  })
  console.log(`📊 累计 CLS: ${cls.toFixed(3)}`)
})

clsObserver.observe({ type: 'layout-shift', buffered: true })

抓到某个广告位动态插入导致页面跳动,加上 min-height 占位后 CLS 从 0.35 降到 0.05。

要点总结

API监控目标关键字段
longtask主线程卡顿 >50msduration, attribution
largest-contentful-paintLCP 性能element, renderTime
resource资源/请求耗时initiatorType, duration, 各阶段耗时
paintFP / FCPstartTime
layout-shiftCLS 布局偏移value, sources

⚠️ 注意事项

  1. buffered: true 可以拿到注册之前的性能条目——这对首屏分析至关重要
  2. 用完记得 disconnect(),避免性能开销
  3. 并非所有浏览器都支持所有类型,生产环境加 try-catch
  4. 搭配 PerformanceServerTiming 可以拿到服务端下发的自定义指标

一句话总结:PerformanceObserver 是最优雅的「无侵入性能监控方案」——等线上用户反馈卡顿的时候,数据早就抓好了。