PerformanceObserver
Subscribe to performance entries safely: entry types, buffered flag, observe patterns, and RUM use.
- 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
- Observing unsupported types throws — try/catch or check
supportedEntryTypes. - Dropping entries under load — keep handlers tiny; enqueue to
requestIdleCallback/ batch beacon. - PII in resource URLs — scrub query tokens before send.
- 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.
Related
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
- Event Timing for INPEvent Timing API fields that power INP: input delay, processing, presentation delay, and attribution.
- Layout Instability API CLSLayoutShift entries, value calculation, hadRecentInput, and how to attribute CLS in the field.
- BFCache Back Forward CacheHow the back/forward cache freezes pages for instant history nav, what blocks it, and how to restore state safely.
- Browser DevTools Network PanelRead waterfalls, timing phases, headers, throttling, and initiator chains in the Network panel like a production debugger.
- Browser DevTools Performance PanelRecord main-thread timelines: long tasks, style/layout/paint, frames, and how to turn flame charts into INP fixes.