b2KIT

Performance Observer Playground

Use the Performance Observer API to monitor paint timing, layout shifts, and long tasks in real time.

Tested tool guide Tested browser tools Checked August 16, 2026

What Performance Observer Playground does, with a checked example

The Performance Observer API hands your page a log of its own performance: paints, largest-contentful-paint candidates, layout shifts, long tasks, and input events. This playground attaches an observer to a live demo page in your own browser and streams every matching entry as it fires, showing raw fields like name, startTime, duration, and type-specific extras, so you can learn the API before writing production code. The surprise most people hit: observers are blind to the past. An observer created after an event fired sees nothing unless you pass buffered: true.

Worked example

A concrete input and expected output from the current implementation.

Input

const po = new PerformanceObserver(list => {
  for (const e of list.getEntries()) {
    console.log(e.name + ': startTime ' + Math.round(e.startTime) + ' ms, duration ' + e.duration);
  }
});
po.observe({ type: 'paint', buffered: true });

Expected output

first-paint: startTime 312 ms, duration 0
first-contentful-paint: startTime 342 ms, duration 0

buffered: true makes the observer replay paint entries recorded during page load, in startTime order. first-paint always fires before or together with first-contentful-paint, both carry duration 0, and the millisecond values vary per load, so only the names, ordering, and zero durations are portable.

How the result is produced

1

Selection and delivery

You pick entry types, and the tool registers a PerformanceObserver for each against the demo page. Matching entries arrive in batches, never one at a time, so the playground calls getEntries() and renders each entry's fields (name, startTime, duration, plus type-specific extras like layout-shift value or task attribution) as a row in a live table, with buffered: true entries replayed immediately at registration.

2

Emission is the browser's decision

Paint emits first-paint and first-contentful-paint once each; layout-shift emits only when layout actually moves; longtask fires only for main-thread tasks over 50 ms; largest-contentful-paint re-fires whenever a new largest element appears. The observer cannot produce an entry for an event that never happened, which is why an empty table is itself a finding.

Good uses

  • Reproduce a CLS bug: load a page where an image has no reserved dimensions, watch the layout-shift row appear with its value and hadRecentInput flag as the image lands, then add width/height and confirm the row stops firing.
  • Validate which element wins LCP: with largest-contentful-paint observed live, scroll or inject content and watch the entry re-fire with a new element and size, showing how the final LCP is the last entry in the stream.
  • Audit main-thread responsiveness: run your page's heavy scripts and filter the longtask rows; entries over 50 ms report duration and frame attribution (containerSrc), so you can see which frame or iframe ate the time.

Limits and checks

  • An empty table is not proof of a fast page. No longtask rows can mean no single task over 50 ms, but also an observer registered after the work ran, or work executing in a web worker, which longtask never reports. Check buffered: true and where the work runs before concluding anything.
  • Not every layout-shift row counts toward CLS. Shifts flagged hadRecentInput: true are excluded from Cumulative Layout Shift, and a buffered replay shows the final LCP while live rows show earlier candidates - the two views answer different questions.
  • The numbers are per-session, not portable. startTime is milliseconds since navigation start on your machine and will not match a lab or another device; paint entries always show duration 0, which is normal. Compare entry shapes and ordering, not absolute values.

Common questions

I scrolled and typed a lot, but no layout-shift entries appeared. Is my page stable?

Not necessarily. Scrolling does not create layout-shift entries, and shifts that follow recent user input are still reported but flagged hadRecentInput: true, so they would still show as rows. A truly empty table means either the shifts happened before the observer registered (add buffered: true) or the layout genuinely never moved.

Can I capture entries that fired before I pressed the button?

Yes - that is what buffered: true is for. It replays entries of that type recorded since navigation started. Two limits: it reaches back only to the start of the current page load, and the browser's entry buffer can drop old entries when it overflows (common with resource entries), so replaying everything is not guaranteed.

References and verification

The example and behavioral notes were checked against the browser implementation. Standards and primary references below define the relevant format, formula, or platform behavior.

Related Tools