Guide

Long Animation Frames API explained

Harbor Commerce's checkout step regressed to 240 ms INP p75 after enabling a third-party address-autocomplete widget. Existing longtask observers flagged 190 ms main-thread blocks on the shipping form, but attribution fields were empty — engineers knew when the thread stalled, not which script owned the work. Registering a Long Animation Frames observer on long-animation-frame entries surfaced a script stack: the autocomplete bundle synchronously normalized 1,200 postal codes on every keystroke. Lazy-loading the widget after the Pay button entered view and debouncing normalization cut blocking duration from 168 ms to 22 ms and restored INP p75 to 175 ms within one sprint.

The Long Animation Frames API (LoAF) extends the browser performance timeline with frames where rendering or JavaScript blocked the next paint beyond a threshold (typically 50 ms). Unlike coarse longtask entries, LoAF exposes per-script attribution: invoker type (classic script, module, event listener, promise), source URL, function name, and how much of the frame each script blocked. It is the missing link between INP regressions and actionable fixes. This guide covers LoAF semantics, Performance Observer integration, reading script timing info, the Harbor Commerce refactor, a technique decision table versus longtask and vendor profilers, pitfalls, and a production checklist alongside our Web Workers guide.

From long tasks to long animation frames

The longtask entry type fires when the main thread is busy for more than 50 ms without yielding to the compositor. It helped teams discover jank but often reported a single opaque block with weak or missing attribution — especially for event-handler chains, dynamic imports, and third-party tags injected without PerformanceScriptTiming support.

Long animation frames align with how the browser actually schedules work: a frame bundles style, layout, paint, and JavaScript callbacks that must complete before the next visual update. A LoAF entry records:

  • duration — Total frame time (ms).
  • blockingDuration — Portion that blocked presentation — the number that correlates most directly with perceived lag and INP processing delay.
  • scripts — An array of PerformanceScriptTiming objects, each with sourceURL, sourceFunctionName, invoker, invokerType, startTime, duration, and windowAttribution when cross-frame.
  • renderStart / styleAndLayoutStart — Phase markers separating script work from style/layout/paint within the same frame.

Think of longtask as “something was slow” and LoAF as “this listener in autocomplete.min.js blocked 140 ms of the frame.”

Observing LoAF with Performance Observer

LoAF reuses the same Performance Observer infrastructure. Feature-detect before subscribing:

if (PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
  const loafObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.blockingDuration < 50) continue;
      for (const script of entry.scripts ?? []) {
        report({
          blocking: entry.blockingDuration,
          url: script.sourceURL,
          fn: script.sourceFunctionName,
          invoker: script.invokerType,
          ms: script.duration,
        });
      }
    }
  });
  loafObserver.observe({ type: 'long-animation-frame', buffered: true });
}

Invoker types and blame chains

invokerType tells you how the script entered the frame:

  • user-callback — Event listeners (click, input, scroll). Most INP regressions land here.
  • classic-script / module-script — Parse/eval or top-level module execution during navigation.
  • resolve-promise — Microtasks and promise chains after fetch resolves.
  • request-animation-frame — rAF callbacks that grew too heavy.

When multiple scripts appear in one frame, sort by duration descending and attribute the top contributor first. Cross-origin scripts without Timing-Allow-Origin may redact URLs — still useful as “unknown third-party,” but push vendors to enable timing headers.

Connecting LoAF to INP and Core Web Vitals

INP measures the slowest interaction latency on a page visit: input delay, processing time, and presentation delay. LoAF does not replace INP measurement — you still observe event entries for the metric itself — but it explains why processing time spiked.

Practical workflow:

  1. Detect INP regression via event observer or web-vitals on the affected route.
  2. Filter LoAF entries whose startTime overlaps the slow interaction's processingStartprocessingEnd window.
  3. Rank scripts by blockingDuration; open the named bundle at the attributed function.
  4. Fix: yield with scheduler.yield() where supported, chunk work, defer to requestIdleCallback, or move CPU to a worker.
  5. Re-verify INP p75 on the same route after deploy.

LoAF also catches jank that never triggers a user-visible interaction — heavy rAF loops that steal frames from scrolling. Pair with code splitting so attribution points at a deferrable chunk rather than your critical path bundle.

Harbor Commerce checkout refactor

Harbor Commerce added address autocomplete to reduce form abandonment. Lab Lighthouse INP stayed green; production mobile INP on /checkout/shipping climbed 80 ms over two weeks.

Instrumentation

  • Buffered event observer for INP with route and release tags.
  • New long-animation-frame observer alongside existing longtask — same beacon pipeline.
  • Dashboard column: top script URL by summed blockingDuration per route per week.

Findings

  • Autocomplete normalization — 140 ms blocking on input events; fixed with 150 ms debounce and Web Worker normalization for bulk paste.
  • Pay-button click handler — LoAF showed 60 ms in JSON.stringify on cart state; replaced with incremental diff object.
  • Post-deploy rAF chart resize — 45 ms blocking from D3 axis recalculation on orientation change; moved resize to CSS container queries and throttled rAF to 2 Hz off-checkout.

Longtask counts alone had risen 12% — actionable only after LoAF named scripts. INP p75 on shipping dropped from 240 ms to 175 ms; support tickets mentioning “laggy checkout” fell 30% month-over-month.

Technique decision table

Approach Attribution depth Best when Watch out for
Long Animation Frames (LoAF) Per-script URL, function, invoker, blocking ms INP triage, third-party blame, event-handler profiling Chromium-first; cross-origin URL redaction without TAO
longtask observer Coarse block; attribution when browser provides it Baseline jank detection, older browser fallback Often no script name; hard to prioritize fixes
event observer (INP) Interaction latency phases, not script stacks Metric collection, SLO dashboards Tells you latency, not root cause
Chrome DevTools Performance panel Full flame chart, local only Deep single-session debugging Not scalable to production traffic
JS self-profiling API Sampling stacks in-page Custom profilers, games, WASM apps Overhead, privacy review, limited support
RUM vendor (Datadog, Sentry) LoAF integration + session replay Team already on vendor; cross-browser reporting Cost, script weight, LoAF coverage varies by vendor

Common pitfalls

  • Assuming LoAF equals INP — Measure INP with event entries; use LoAF only for diagnosis.
  • Ignoring sub-50 ms frames — Many small blocks add up; tune your reporting threshold per route sensitivity.
  • Redacted third-party URLs — Ask vendors for Timing-Allow-Origin: * or host scripts first-party.
  • Heavy observer callbacks — Serialize and beacon asynchronously; never parse stacks synchronously in the callback.
  • Missing buffered: true — Late analytics tags miss early-frame blocks after navigation.
  • Fixing the wrong script — Sort by blockingDuration, not total duration; layout/paint time may be a symptom of earlier script bloat.
  • No release attribution — Without version tags you cannot tie a spike to a deploy.
  • Safari-only traffic — LoAF is Chromium-centric today; keep longtask fallback and lab profiling for other engines.

Production checklist

  • Feature-detect long-animation-frame in PerformanceObserver.supportedEntryTypes.
  • Register buffered LoAF observer on checkout, editor, and dashboard routes.
  • Beacon top three scripts by blockingDuration per slow interaction.
  • Correlate LoAF timestamps with event entry processing windows.
  • Require Timing-Allow-Origin on first-party and contracted third-party scripts.
  • Alert when any single script exceeds 100 ms blocking on a critical route.
  • Keep longtask observer as fallback for non-Chromium browsers.
  • Document fixes in release notes; re-check INP p75 within 48 hours of deploy.
  • Sample beacons on high-traffic pages to control ingestion cost.
  • Review third-party scripts quarterly; remove tags with persistent top-blame ranking.

Key takeaways

  • LoAF adds long-animation-frame entries with per-script attribution — the fastest path from INP regression to a named bundle and function.
  • blockingDuration is the frame metric that matters most for perceived lag; scripts[] tells you who to fix.
  • Use Performance Observer with buffered: true alongside event entries — LoAF diagnoses, INP measures.
  • Harbor Commerce cut checkout INP 65 ms by debouncing autocomplete and worker-offloading normalization after LoAF named the culprit.
  • Keep longtask fallback for Safari and redacted cross-origin scripts; push vendors for Timing-Allow-Origin.

Related reading