跪拜 Guibai
← Back to the summary

Five PerformanceObserver Patterns That Catch What Logging Misses

🎯 Web Performance API Collection: 5 Lesser-Known Uses of Performance Observer

Performance Monitoring | Browser APIs | You thought the Performance API was just performance.now()? PerformanceObserver can listen for LCP, FMP, long tasks, memory leaks... and even capture "who is secretly running expensive synchronous operations" — all without blocking the main thread.

The Problem Scenario

An online project occasionally stutters, but the issue can't be reproduced in DevTools. Instrumented logs also reveal nothing — because you can only see the result (the page is slow), not the cause (what is occupying the main thread).

// Traditional instrumentation
const start = performance.now()
doHeavyWork()
console.log(`Duration: ${performance.now() - start}ms`)

This approach has two fatal flaws:

  1. High invasiveness — requires modifying business code.
  2. Cannot catch sporadic issues — you don't know when to instrument.

The Solution: PerformanceObserver

PerformanceObserver is a performance API based on the observer pattern. It can subscribe to multiple performance event types and execute callbacks asynchronously when events occur, with zero invasiveness.

1️⃣ Capturing All Long Tasks

Which functions monopolize the main thread for more than 50ms? Catch them directly:

const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    console.warn(`⚠️ Long Task ${entry.duration.toFixed(0)}ms`, {
      name: entry.name,
      startTime: entry.startTime,
      duration: entry.duration,
      // attribution contains specific function info (Chrome)
      culprit: entry.attribution?.[0]?.toJSON()
    })
  }
})

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

Example output:

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

💡 Combined with attribution, you can pinpoint which iframe or Worker triggered the long task.

2️⃣ Monitoring LCP (Largest Contentful Paint)

LCP is a Core Web Vitals metric. Using PerformanceObserver, you can know exactly which element is the LCP element:

const lcpObserver = new PerformanceObserver(list => {
  const entries = list.getEntries()
  const lastEntry = entries[entries.length - 1]
  console.log(`🎨 LCP Element: ${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 })

Real-world scenario: Discovered the LCP element was a background-image. Investigation revealed the image lacked a preload. Adding <link rel="preload"> reduced LCP from 4.2s to 1.8s.

3️⃣ Automatically Tracking Resource Load Timings

Want to know why a specific API request is slow? No need to modify fetch code:

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 })

Example output:

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

At a glance, the bottleneck is TTFB (slow server response), not the network.

4️⃣ Monitoring 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️⃣ Capturing Layout Shifts (CLS Metric)

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(`📊 Cumulative CLS: ${cls.toFixed(3)}`)
})

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

Caught a dynamically inserted ad slot causing page jitter. Adding a min-height placeholder reduced CLS from 0.35 to 0.05.

Key Takeaways Summary

API Monitoring Target Key Fields
longtask Main thread blocking >50ms duration, attribution
largest-contentful-paint LCP performance element, renderTime
resource Resource/request timing initiatorType, duration, phase timings
paint FP / FCP startTime
layout-shift CLS layout shifts value, sources

⚠️ Important Notes

  1. buffered: true can retrieve performance entries from before registration — this is critical for first-screen analysis.
  2. Remember to disconnect() when done to avoid performance overhead.
  3. Not all browsers support all types; wrap in try-catch for production.
  4. Combined with PerformanceServerTiming, you can retrieve custom metrics sent from the server.

In one sentence: PerformanceObserver is the most elegant "non-invasive performance monitoring solution" — by the time users report lag, the data is already captured.