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

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

问题场景

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

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

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

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

解决方案:PerformanceObserver

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

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

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

js 复制代码
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 })

执行结果示例:

yaml 复制代码
⚠️ 长任务 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 元素:

js 复制代码
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 代码:

js 复制代码
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 })

执行结果示例:

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

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

4️⃣ 监控 First Paint / First Contentful Paint

js 复制代码
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 指标)

js 复制代码
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 主线程卡顿 >50ms duration, attribution
largest-contentful-paint LCP 性能 element, renderTime
resource 资源/请求耗时 initiatorType, duration, 各阶段耗时
paint FP / FCP startTime
layout-shift CLS 布局偏移 value, sources

⚠️ 注意事项

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

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

相关推荐
用户31693538118315 小时前
SpringBoot + Vue 项目生产环境部署完整指南
前端·后端
Csvn15 小时前
🔍 TypeScript `satisfies` 操作符:既要类型安全,又要推断的原始字面量类型
前端
Csvn16 小时前
🚦 手写 Promise 并发控制:如何优雅限制异步请求的并发数?
前端
gb421528716 小时前
python中Web应用服务器
开发语言·前端·python
默_笙16 小时前
💌 为了把"主题色"传给孙子组件,我翻了五层楼——直到遇见了 useContext
前端·javascript
颜进强16 小时前
04 - 一张图看懂 OpenSpec 变更的一生:从创建到归档
前端·后端·ai编程
the_answer16 小时前
JS 垃圾回收机制
前端
小KK_16 小时前
JS 事件循环从小白视角入门:宏任务、微任务与 async/await 一网打尽
前端·javascript
aloha_16 小时前
Linux服务器上指定目录的文件下载
前端
szp200517 小时前
为了在浏览器里跑多线程 ONNX 推理,我把自己的支付浮层弄挂了
前端·webassembly