A PerformanceObserver for the 'longtask' entry type reports many 50ms-plus tasks in production, but the entries do not reveal which code is responsible. What does a longtask entry actually give you, and what does the 'long-animation-frame' entry type add?
answer
- it tells you when, not who
- attribution points at a frame, not a script
- the newer type measures a slow frame
- a scripts array with source URLs
- forced style and layout, visible in the field
basics
~20 sA longtask entry gives you only a start time, a duration and a coarse attribution to the containing frame or iframe — never a script or function. The newer long-animation-frame entry type reports the whole slow frame, including a scripts array with each script's source URL, invoker and duration.
solid answer
~50 s`longtask` entries are deliberately thin. You get `startTime`, `duration` (always at least 50 ms) and an `attribution` array of `TaskAttributionTiming` objects whose `containerType`, `containerSrc`, `containerId` and `containerName` identify *which frame* the work came from — the top-level document or a particular iframe. That is enough to say "a third-party iframe is burning the main thread" and nothing more; for your own code it tells you a long task happened somewhere in your document. The `long-animation-frame` entry type, added later in Chromium, changes the unit of measurement from a task to a frame that took too long to render, and reports the frame's internals: `renderStart`, `styleAndLayoutStart`, `blockingDuration`, and crucially a `scripts` array of `PerformanceScriptTiming` entries carrying `sourceURL`, `sourceFunctionName`, `invoker`, `invokerType`, `duration` and `forcedStyleAndLayoutDuration`. That last field is how a synchronous-layout problem shows up in field data rather than only under a local profiler. Both are Chromium-only, so treat them as diagnostics that cover part of your traffic, not universal metrics.
code
javascript · 19 linesconst worst = [];
if (PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) {
new PerformanceObserver((list) => {
for (const frame of list.getEntries()) {
if (frame.blockingDuration < 50) continue;
for (const script of frame.scripts) {
worst.push({
url: script.sourceURL,
fn: script.sourceFunctionName,
invoker: script.invokerType,
ms: Math.round(script.duration),
forcedLayoutMs: Math.round(script.forcedStyleAndLayoutDuration)
});
}
}
worst.sort((a, b) => b.ms - a.ms).splice(5);
}).observe({ type: 'long-animation-frame', buffered: true });
}go deeper
Know that the browser can tell you a block of main-thread work ran long, that the threshold is 50 milliseconds, and that the entry alone does not identify which code caused it.
Explain what the attribution array actually contains — container type, source and id, identifying a frame rather than a script — and why the newer frame-based entry type reports a scripts array instead.
Show how you would run the investigation from field data: filtering on blocking duration, reading script attribution and forced style-and-layout time, and keeping the collection cheap enough not to add to the jank.
Own how partial-coverage diagnostics enter decision-making: what may be a target versus a debugging signal, how the third-party attribution asymmetry is disclosed, and what you commit to when the data only covers part of your users.
## What longtask reports and why it is so sparse The Long Tasks API defines a long task as an uninterrupted block of main-thread work of 50 ms or more, and surfaces one entry per occurrence: ```javascript new PerformanceObserver((list) => { for (const task of list.getEntries()) { console.log(task.startTime, task.duration, task.attribution[0]?.containerSrc); } }).observe({ type: 'longtask', buffered: true }); ``` An entry carries `name`, `startTime`, `duration` and `attribution`. Each `TaskAttributionTiming` in that array describes a *container*: `containerType` (`iframe`, `embed`, `object`, or the window itself), plus `containerSrc`, `containerId` and `containerName` taken from the element's attributes. Note what is absent: no script URL, no function name, no stack. That is not an oversight. Exposing which cross-origin script consumed the main thread would be a cross-origin information leak, and the API was specified conservatively. The consequence in practice is that a dashboard of long tasks tells you *how much* main-thread blocking your users experience and *when*, which is genuinely useful for tracking a trend, but it cannot answer "whose code". ## What Long Animation Frames changed LoAF reframes the question. Users do not perceive tasks; they perceive frames that did not update. A long animation frame is a rendering update that took too long — measured from the start of the work that preceded it through to the end of rendering — so it captures work that long tasks miss, including many short tasks that together blocked a frame, and the style and layout work at the end of the frame, which is not part of any script task at all. The entry is much richer: - `startTime`, `duration` — the frame's extent. - `renderStart`, `styleAndLayoutStart` — where rendering and then style/layout began inside it. - `blockingDuration` — how much of the frame counted as blocking, which is the number that correlates with felt jank. - `firstUIEventTimestamp` — when the earliest queued UI event arrived, letting you tie a slow frame to an interaction that was waiting on it. - `scripts` — an array of `PerformanceScriptTiming` entries. Each script entry carries `sourceURL`, `sourceFunctionName`, `sourceCharPosition`, `duration`, `invoker` and `invokerType` (was this a classic script evaluation, an event listener, a promise resolution, a timer callback?) and `forcedStyleAndLayoutDuration` — time this script spent forcing style and layout synchronously. ```javascript new PerformanceObserver((list) => { for (const frame of list.getEntries()) { if (frame.blockingDuration === 0) continue; for (const script of frame.scripts) { report(script.invokerType, script.sourceURL, script.duration); } } }).observe({ type: 'long-animation-frame', buffered: true }); ``` That is field-grade attribution: a production dashboard can name the file and the invoking mechanism, which is the gap that made long-task data frustrating to act on. ## Cross-origin limits still apply LoAF is more generous but not unlimited. Script attribution for cross-origin code is coarsened — you may get a marker rather than a precise source location and function name — for the same information-leak reason that kept long tasks sparse. Your own first-party bundles attribute properly; a third-party tag may not, and that asymmetry is worth stating explicitly when you present the data, otherwise the numbers read as "our code is the whole problem". ## Availability shapes how you use it Neither API is universally implemented; both are Chromium features, and `long-animation-frame` is the newer of the two. Feature-detect with `PerformanceObserver.supportedEntryTypes` and treat the output as a diagnostic signal over the portion of traffic that supports it. A metric you can only measure on some browsers is fine as a debugging input and dangerous as a headline number, because the population it covers is not the population you serve. ## How this fits an investigation The workflow these APIs enable is: field data shows a responsiveness problem; long-task or LoAF volume tells you main-thread contention is the cause rather than the network; LoAF's `scripts` array names the file and the invoker; `forcedStyleAndLayoutDuration` distinguishes "this script computes too much" from "this script keeps forcing the browser to recompute style and layout". Those are different fixes, and being able to tell them apart from production data — rather than only from a local recording of a page you have already made fast — is the real value. ## Volume and cost Observing either type on a busy page delivers a lot of entries, and your callback runs on the main thread you are trying to protect. Filter early — skip frames with zero `blockingDuration`, keep only the worst N per page — and aggregate before sending. Instrumentation that measures jank while contributing to it is a real, and slightly embarrassing, failure mode.
- Why does the Long Tasks API refuse to name the script responsible?Cross-origin information leakage. Reporting that a specific third-party script consumed the main thread would expose behaviour of code from another origin to the embedding page, so the API was specified to reveal only the containing frame. Long Animation Frames relaxes this for same-origin scripts, where the embedding page already has the source, while still coarsening attribution for cross-origin code.
- What can a long-animation-frame entry capture that a longtask entry cannot?Several things: style and layout work at the end of a frame, which belongs to no script task; a frame blocked by several short tasks that individually stay under the 50 ms bar; and the identity of the scripts involved, via the `scripts` array. It also exposes `forcedStyleAndLayoutDuration` per script, which surfaces synchronous layout in production rather than only under a local profiler.
- How would you keep this instrumentation from becoming a performance problem itself?Filter and aggregate inside the observer callback rather than shipping raw entries. Drop frames with zero blocking duration, retain only the worst few per page view, and reduce to counts and totals keyed by source URL before beaconing at page hide. The callback runs on the main thread you are protecting, so per-entry network calls or heavy object construction there are self-defeating.
- How should you present a metric that only Chromium browsers report?As a diagnostic over a named subset, never as a headline. State the covered share of traffic alongside the number, and do not compare it against metrics collected across all browsers as if the denominators matched. It is excellent for finding and fixing a cause, and misleading as a target, because improvements or regressions in the unmeasured population are invisible.
saying these in an interview costs you the question
- Expects a longtask entry to name the script or function
- Reads container attribution as script attribution
- Assumes every browser reports long tasks
- Sends every long-task entry to the network as it arrives
- Thinks long tasks capture all frame-blocking work