Performance
PerformanceObserver in practice: what to watch and what not to
PerformanceObserver reads performance entries without polling. Chrome 155 exposes 15 entry types, 7 of them buffered: which ones matter.
Key points
- PerformanceObserver receives entries when the browser produces them, with no timer.
- Chrome 155 exposes 15 entry types and 7 return entries with buffered: true.
- Without buffered you lose LCP and the long tasks emitted before the script started.
- Type support differs per browser: read it, never assume it.
PerformanceObserver receives performance entries as the browser produces them, with no timer: with buffered you also recover the entries emitted before the script started.
What is worth watching
| Entry type | What it holds | Buffered | Use |
|---|---|---|---|
| largest-contentful-paint | Time and element of the largest paint | yes | field LCP |
| event | Duration and processingStart of an event | no | INP diagnosis |
| longtask | Blocks over 50 ms | yes | attributing the blocks |
| layout-shift | Shifts and the nodes that move | no | CLS diagnosis |
| resource | Timing and bytes per request | yes | weight and cache |
| navigation | Document timing | yes | TTFB and FCP |
Six types cover LCP, INP and CLS. The rest aid diagnosis: element and mark measure what your own code marks, and visibility-state tells you when the tab is hidden, which is useful to discard entries recorded in a background tab.
The pattern that holds
new PerformanceObserver((list) => {
const body = JSON.stringify(
list.getEntries().map((e) => ({
name: e.name,
start: Math.round(e.startTime),
dur: Math.round(e.duration),
})),
);
navigator.sendBeacon("/api/rum", new Blob([body], { type: "application/json" }));
}).observe({ type: "longtask", buffered: true });
The code needs a browser with sendBeacon and an endpoint that accepts POST; without an endpoint replace the call with console.log. One observer per type, not one per event: the callback already receives a list.
Three common mistakes
- Observing without buffered: the entries emitted before the script never arrive.
- Observing layout-shift without filtering hadRecentInput: shifts caused by a click do not count for CLS.
- Forgetting disconnect() in a SPA: every navigation adds an observer and the entries duplicate.
The limitation to name
The set of types depends on the browser. Chrome 155 exposes 15, Firefox and Safari expose fewer and some names do not exist; an observer that assumes longtask on Safari receives nothing and throws no error. Check coverage with PerformanceObserver.supportedEntryTypes instead of assuming it.
Where to continue
The EneUI catalogue indexes 111,985 components, 12,808 icons and the record image auto slider: a slider that rotates on its own produces entries even when nobody watches it. For the metric picture read what Core Web Vitals are, for responsiveness INP in practice, and for the comparison between hosted and self-hosted analytics Vercel Analytics vs self-hosted.
Frequently asked questions
Does PerformanceObserver replace Lighthouse?
No. Lighthouse measures a page in the lab, PerformanceObserver reads what the browser produces during a real visit.
What does buffered: true do?
It asks the browser for the entries already emitted before the observer started. Without it you lose the LCP and the long tasks of the load.
Which types do I need for Core Web Vitals?
largest-contentful-paint for LCP, event for INP and layout-shift for CLS. The other types help explain the cause.
Why does my observer receive nothing on Safari?
Because that type is not supported. Check supportedEntryTypes and write a fallback branch before observing.
How do I avoid duplicate entries?
With a flag or a Set inside the callback, and with disconnect() when the component unmounts. With reportAllChanges entries arrive more than once.