LoAF vs the Long Tasks API: Which Observer Should Feed Your INP Debugging?

This comparison belongs to the Long Animation Frames topic in Core Web Vitals & Measurement. Both APIs report main-thread congestion above the 50ms threshold, and many RUM setups still collect longtask entries out of habit. The question is whether that data can still earn its beacon bytes now that long-animation-frame exists.

The short answer: for diagnosing Interaction to Next Paint, LoAF supersedes Long Tasks in Chromium. The longer answer depends on what else you use task data for — Total Blocking Time approximations, cross-browser coverage plans, or historical dashboards you do not want to break.

What each observer actually tells you Side-by-side comparison of what a long task entry and a long animation frame entry report for the same slow interaction. What each observer actually tells you longtask entry • One task over 50ms, start and duration • attribution usually reads unknown or window • Rendering work after the task is invisible • Several 40ms tasks in one frame are never reported long-animation-frame entry • Whole frame from first task to paint • Each script with URL, function and invoker • renderStart and styleAndLayoutStart for the tail • Forced layout time per script

Rapid Diagnosis: Is Your Current Data Actionable?

  • Inspect a week of longtask attribution. If more than 90% of entries carry name: "self" and a container of window, your long-task data identifies that the thread was busy and nothing more.
  • Look for slow interactions without long tasks. An INP of 260ms composed of five 40ms tasks produces no longtask entries at all — but one LoAF entry.
  • Check whether presentation delay is ever attributed. Long Tasks cannot see style, layout and paint after your script returns; if your INP breakdown shows presentation delay dominating, you have been blind to its cause.
  • Count browsers. If a large share of your RUM comes from Safari and Firefox, neither API covers it; the choice between them is a Chromium-only decision.

The Comparison

AxisLong Tasks APILong Animation Frames API
UnitOne task over 50msOne frame over 50ms
Script attributionContainer only, usually unhelpfulURL, function, char position, invoker
Rendering visibilityNonerenderStart, styleAndLayoutStart
Forced layoutNot reportedPer script
Many short tasksMissed entirelyCaptured as one long frame
Blocking metricDuration over 50ms per taskblockingDuration per frame
SupportChromiumChromium 123+

Root Cause Analysis: Why Long Tasks Misled Teams

1. The task is the wrong unit for an interaction. A user experiences the delay between input and the next paint. That span can include several tasks and a rendering step. A metric that only fires when one task exceeds 50ms systematically misses the "death by several cuts" pattern common in framework apps, where each microtask batch is modest.

2. Attribution was structurally limited. Long Tasks attribute to a container — the frame or iframe — not to a script. For first-party work in the top frame, that is always "the page". Teams built elaborate timestamp-correlation heuristics to compensate; LoAF makes them unnecessary.

3. Rendering was invisible. A handler that returns in 20ms but invalidates layout for 10,000 nodes produces no long task; the expensive work happens in the browser's rendering step. LoAF reports it as the render tail of the frame.

Five short tasks, no long task, one long frame A timeline in which five tasks each under 50 milliseconds plus rendering add up to a 230 millisecond frame that only LoAF reports. Five short tasks, no long task, one long frame Tasks 40ms 40ms 45ms 35ms 35ms render Reported one long-animation-frame entry (230ms) 0ms 50ms 100ms 150ms 200ms 250ms INP 200ms The Long Tasks API reports nothing here because no single task crosses 50ms.

Step-by-Step Migration

1. Add a LoAF observer next to the existing one

Run both for a release so you can compare coverage before deleting anything.

javascript
const supportsLoAF = PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame');
if (supportsLoAF) {
  new PerformanceObserver((l) => queueLoAF(l.getEntries()))
    .observe({ type: 'long-animation-frame', buffered: true });
} else {
  new PerformanceObserver((l) => queueLongTasks(l.getEntries()))
    .observe({ type: 'longtask', buffered: true });
}
// trade-off: falling back to longtask on non-LoAF engines gains nothing today —
// the engines without LoAF also lack longtask. The branch only helps older
// Chromium builds; drop it once their share of your traffic is negligible.

Expected outcome: within a day, you can see how many slow interactions had zero longtask entries but a LoAF entry — usually a meaningful share on framework-heavy pages.

2. Rebuild your blocking-time approximation on blockingDuration

If dashboards chart a field "TBT-like" figure from long tasks, switch to summing blockingDuration across frames between FCP and a cutoff.

javascript
let fieldBlocking = 0;
function queueLoAF(entries) {
  for (const f of entries) if (f.startTime < 10_000) fieldBlocking += f.blockingDuration;
}
// trade-off: blockingDuration includes rendering time while longtask-derived
// TBT did not, so the new series runs higher. Annotate the dashboard on the
// switch date rather than comparing the two series directly.

Expected outcome: a blocking figure that tracks INP input delay more closely than the old one.

3. Retire the long-task beacon fields

Once a release has shipped with both, remove longtask collection from the beacon. Most teams save 200–600 bytes per beacon on heavy pages.

javascript
// Before: { longTasks: [{start, duration, attribution: 'unknown'}, ...] }
// After:  { loaf: [{start, duration, blocking, topScript: {src, fn, ms}}] }
// trade-off: if a third-party RUM vendor still computes metrics from longtask
// entries it reads directly from the page, removing YOUR observer does not
// affect it — but check their docs before assuming their charts are unchanged.

Verification

Pick five interactions that RUM marks as slow and reproduce them in the lab with both observers running. For each, confirm the LoAF entry's script list names the code you would have blamed after manual profiling. If LoAF consistently identifies the culprit and Long Tasks either reports nothing or "unknown", the migration is justified. Then confirm beacon payload size dropped and that the INP attribution coverage — the share of INP beacons with a named top script — rose above roughly 70%.

When Long Tasks Still Earn Their Place

Keep a longtask observer only if you have a long-running historical chart you cannot re-baseline, or a Lighthouse-style TBT comparison that requires task semantics. For new instrumentation there is no case for it: LoAF reports a superset of the problem with attribution that actually names code.

Keep, migrate or drop the longtask observer A decision sequence for whether to keep collecting long task entries after adopting the Long Animation Frames API. Keep, migrate or drop the longtask observer Historical chart you cannot re-baseline? Keep longtask for one overlap quarter, then drop yes no Debugging INP attribution? Use LoAF only yes no Need a field TBT proxy? Sum LoAF blockingDuration yes no Drop longtask collection entirely

Cost and Overhead Compared

Neither API adds meaningful work to the browser itself — both are recorded internally whether or not anyone observes them. The difference is in what your code does with the entries.

A longtask entry is small and flat: a name, a start time, a duration, and a short attribution array. A LoAF entry is larger, and its scripts array can hold a dozen nested objects with long URL and invoker strings. On a busy page that produces 30 long frames during load, naive collection allocates several hundred objects that sit in memory until the beacon is sent. That is negligible on a laptop and measurable on a low-end phone with an aggressive garbage collector.

The practical rule is to filter in the observer callback, not later: discard frames under 50ms, discard frames with no overlap with any interaction once the page is interactive, and keep only the top scripts of the frames you retain. With that discipline the memory footprint of LoAF collection is within a few kilobytes of the old long-task observer while carrying far more useful information.

Throughput is similar. Observer callbacks are batched and delivered asynchronously, so neither API runs code inside the long frame itself. What occasionally surprises teams is that a heavy callback — one that serialises entries to JSON on arrival — becomes a long task of its own and shows up in the next LoAF entry, attributed to your RUM script. If your own beacon code appears in the top-scripts list, move serialisation to the send step.

FAQ

Is Total Blocking Time in Lighthouse affected by LoAF?

No. Lighthouse still computes TBT from the trace's tasks in the lab; LoAF is a field-and-page API. The two are related — both use a 50ms threshold — but a page can have good lab TBT and still produce long animation frames in the field because real devices are slower and real interactions overlap third-party work.

Will other browsers implement LoAF?

At the time of writing only Chromium ships it. That limits it to a diagnostic role: you cannot compute a cross-browser metric from it, but you can find the scripts responsible, and those scripts are usually just as expensive in other engines. Fixes discovered through Chromium attribution typically improve Safari and Firefox users too.