ESC

Type to search the knowledge base.

PerformanceObserver

Subscribe to performance entries safely: entry types, buffered flag, observe patterns, and RUM use.

advanced3 min read
  • browser
  • performanceobserver
  • rum
  • web-vitals

PerformanceObserver is the standard way to stream performance entries (paints, resources, long tasks, layout shifts, events) without polling getEntries in a hot loop. Almost every modern RUM agent and the web-vitals library sit on top of it.

Docs: MDN PerformanceObserver, Performance Timeline.

Basic pattern

const po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    report(entry.toJSON());
  }
});

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

buffered: true

Delivers entries that occurred before the observer was created (subject to buffer limits). Critical for late-loaded analytics scripts that still want LCP.

Common entry types

Type Use
navigation Document timing
resource Subresource timing
paint FP / FCP (legacy paint entries)
largest-contentful-paint LCP candidates
layout-shift CLS
longtask Main-thread blocks
event Event Timing / INP inputs
measure / mark User Timing

Feature-detect: PerformanceObserver.supportedEntryTypes.

if (PerformanceObserver.supportedEntryTypes?.includes('layout-shift')) {
  // observe
}

Multiple types

Older API used entryTypes: ['resource', 'navigation']. Newer code prefers single type per observe call (and multiple observers) for options like durationThreshold.

po.observe({ type: 'event', buffered: true, durationThreshold: 16 });

Disconnect and memory

po.disconnect();

Observers retain callbacks. For SPAs that create observers per route, disconnect on leave or use a singleton RUM bootstrap.

User Timing integration

performance.mark('search:start');
// ... work
performance.mark('search:end');
performance.measure('search', 'search:start', 'search:end');

const measures = new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (e.entryType === 'measure') send('ux', e.name, e.duration);
  }
});
measures.observe({ type: 'measure', buffered: true });

Pitfalls

  1. Observing unsupported types throws — try/catch or check supportedEntryTypes.
  2. Dropping entries under load — keep handlers tiny; enqueue to requestIdleCallback / batch beacon.
  3. PII in resource URLs — scrub query tokens before send.
  4. Assuming 100% browser coverage — degrade gracefully.

Interview out-loud

“PerformanceObserver streams timeline entries with optional buffered replay. I feature-detect supportedEntryTypes, keep callbacks cheap, and build CWV/RUM on LCP, event, layout-shift, resource, and navigation types.”

Sampling strategy

Observe everything in development; in production, sample heavy resource timelines. Always keep CWV observers at high sample rates if volume allows. Drop entries when document.visibilityState is hidden if you only care about foreground UX — but still record CLS that happened while visible.

Error handling template

function observe(type, cb, opts = {}) {
  if (!PerformanceObserver.supportedEntryTypes?.includes(type)) return () => {};
  try {
    const po = new PerformanceObserver((list) => {
      list.getEntries().forEach(cb);
    });
    po.observe({ type, buffered: true, ...opts });
    return () => po.disconnect();
  } catch {
    return () => {};
  }
}

Use this wrapper so a single unsupported type does not break your entire RUM bootstrap bundle.

Further depth

Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.

Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.

Further reading

Related guides