skip to content

A page registers a PerformanceObserver for entryType 'longtask' and receives a 320 ms entry. How much does that entry actually tell you about which code was responsible, and what does the Long Animation Frames API ('long-animation-frame') add?

level: seniorimportance: nice to knowfreq 30%

answer

  1. the entry names a frame, not a function
  2. container, not call site
  3. chunked work emits nothing at all
  4. the unit changes from task to frame
  5. scripts array with sourceURL and function name

basics

~20 s

A longtask entry gives duration, start time and a frame-level attribution — never the script or function responsible. The Long Animation Frames API reports whole slow frames instead of single tasks, with a per-script breakdown including source URL and function name, which is what makes field attribution usable.

solid answer

~50 s

The `longtask` entry is close to useless for attribution. You get `startTime`, `duration`, a `name` saying which browsing context relative to yours was to blame, and an `attribution` array — but that attribution identifies a *container*, an iframe or embed, not a script, function or call site. In real-user monitoring you end up knowing a 320 ms task happened and nothing about why. It also has a blind spot: work already split into sub-50 ms chunks emits no long-task entries even if the frame is still stalled for 320 ms. The Long Animation Frames API changes the unit of measurement from the task to the animation frame — a frame whose rendering update was delayed past 50 ms. Its entries expose `blockingDuration`, `renderStart` and `styleAndLayoutStart`, so you can separate script time from style-and-layout time, plus a `scripts` array with `sourceURL`, `sourceFunctionName`, `sourceCharPosition` and the invoker. That is enough to name the offender from field data. It is Chromium-first, so treat it as a sampling channel, not universal coverage.

code

javascript · 14 lines
javascript
if (PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
  new PerformanceObserver((list) => {
    for (const frame of list.getEntries()) {
      if (frame.blockingDuration < 100) continue;
      const worst = [...frame.scripts].sort((a, b) => b.duration - a.duration)[0];
      console.log({
        blocking: Math.round(frame.blockingDuration),
        renderMs: Math.round(frame.startTime + frame.duration - frame.renderStart),
        source: worst && `${worst.sourceURL}:${worst.sourceFunctionName}`,
        invoker: worst && worst.invokerType,
      });
    }
  }).observe({ type: 'long-animation-frame', buffered: true });
}

go deeper

for a junior

Know that the browser can report slow main-thread work to your own JavaScript through PerformanceObserver, so problems can be measured on real users' devices and not only in a local profile.

for a middle

Be able to register the observer and read the fields, and to state the key limitation: a longtask entry tells you how long, not what. Know that a newer API reports slow frames with script-level detail.

for a senior

Expect to design the field pipeline: what you sample, what you threshold on, what you strip before beaconing, and how you map minified names back. Be honest that the data locates a suspect rather than diagnosing it.

for a principal

Own the decision of whether real-user attribution is worth its cost and privacy surface at all. Be ready to argue what you would alert on, what regression signal you would trust across releases, and how you avoid a telemetry layer that degrades the experience it measures.

## What a longtask entry contains Registering the observer is trivial: ```js new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(entry.name, entry.duration, entry.attribution); } }).observe({ type: 'longtask', buffered: true }); ``` Each entry carries `startTime`, `duration`, an `entryType` of `longtask`, and a `name` — and that `name` is not what most people expect. It is a coarse label for *which browsing context* ran the task relative to the observing one: values such as `self`, `same-origin-descendant`, `cross-origin-descendant`, or `multiple-contexts`. The `attribution` array holds entries describing a **container** — `containerType` (iframe, embed, object, window), plus `containerName`, `containerId` and `containerSrc`. ## Why that attribution disappoints Every one of those fields points at a *frame*, not at code. There is no script URL, no function name, no stack. In a lab trace this hardly matters — you can see the flame chart. In real-user monitoring it is crippling: you collect thousands of entries proving that users experience 200-400 ms tasks, with no way to tell whether the culprit is your data layer, a third-party tag, or the framework's own bootstrap. Teams routinely resorted to wrapping suspicious code in `performance.mark` and `performance.measure` and correlating timestamps by hand. The design was deliberate — exposing precise timing attributed to cross-origin script has privacy implications — but the practical result is a metric you can alert on and cannot act on. ## The second problem: the chunking blind spot Because the unit is the task, work split into eight consecutive 40 ms chunks emits **no** long-task entries at all. If those chunks run back to back and the browser still cannot produce a frame for 320 ms, the user's experience is nearly identical to one 320 ms task, but your instrumentation reports a clean page. The same blind spot tends to hide rendering cost: style, layout and paint work that follows your script is not necessarily surfaced as a single long task, so a page whose frames are dominated by layout can look quiet. ## What Long Animation Frames changes LoAF moves the unit of measurement from the task to the **animation frame**. An entry is emitted for a frame whose rendering update was delayed beyond 50 ms — which naturally captures both the one-big-task case and the many-small-chunks case, and includes the browser's own rendering work rather than only your script. The entry is far richer: - `duration` and `startTime` — the frame's extent; - `blockingDuration` — the portion of the frame that would have prevented responding to input, which is the number to aggregate for responsiveness; - `renderStart` and `styleAndLayoutStart` — boundaries that let you split "my script was slow" from "style and layout were slow"; - `firstUIEventTimestamp` — when an input event was queued during the frame, letting you tie a slow frame to a real interaction; - `scripts` — an array of per-script entries with `sourceURL`, `sourceFunctionName`, `sourceCharPosition`, `invoker`, `invokerType`, `executionStart` and `forcedStyleAndLayoutDuration`. That `scripts` array is the headline. `invokerType` tells you whether the entry point was a classic script evaluation, an event listener, a promise resolution, a timer or a user callback, and `sourceFunctionName` with `sourceCharPosition` gets you to a call site. For the first time, field data can say *which* of your handlers is producing slow frames. ## Using it in real-user monitoring A workable pattern: observe `long-animation-frame` with `buffered: true`, filter to entries whose `blockingDuration` passes a threshold you care about, keep only the top one or two `scripts` entries by duration, and beacon a compact record. Do not ship the whole entry — the `scripts` array can be large, and unbounded telemetry becomes its own performance problem. Sampling a fraction of sessions is normal. Because `sourceURL` and `sourceFunctionName` reflect the *shipped* bundle, minified names are what you will receive; plan to map them back with source maps server-side, or you will be triaging `n` and `t`. ## Caveats worth stating in an interview Both APIs are Chromium-first — Long Animation Frames landed in Chrome 123 in 2024 — so a field dataset built on them describes your Chromium users and silently omits the rest. Feature-detect and degrade rather than assuming coverage: ```js if (PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) { // register the observer } ``` And remember that neither API replaces a trace. They tell you *where* to look, in aggregate, across real devices you do not own. Once you know the function, you still profile it to find out why it is slow.

  • Why does the Long Tasks API refuse to name the script that caused the task?
    Precise timing attributed to cross-origin script is a privacy and side-channel concern — it can leak information about content the observing page is not entitled to read. The API therefore stops at the browsing-context level. Long Animation Frames exposes per-script detail within the constraints the platform now considers acceptable, which is part of why it took a separate specification to get there.
  • You have Long Animation Frame data showing a slow frame with a large forcedStyleAndLayoutDuration inside one script entry. What does that point at?
    It says that script did not just compute — it caused style and layout work to run synchronously while it executed, so the cost sits inside your function rather than in the frame's normal rendering phase. That is a different fix from "this function does too much arithmetic", and it is exactly the distinction the separate rendering timestamps exist to expose.
  • How would you keep this instrumentation from becoming a performance problem itself?
    Sample sessions rather than instrumenting everyone, threshold on blockingDuration so quiet frames cost nothing, keep only the top one or two script entries instead of the whole array, and batch beacons rather than sending per frame. Observer callbacks run on the main thread, so whatever you do inside them is itself main-thread work.

saying these in an interview costs you the question

  • Expects the longtask entry to include a script URL or stack
  • Reads entry.name as the offending function's name
  • Assumes no long-task entries means no slow frames
  • Ships full Long Animation Frame entries to analytics unsampled
  • Treats Long Animation Frames data as covering all browsers

context