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 ofPerformanceScriptTimingobjects, each withsourceURL,sourceFunctionName,invoker,invokerType,startTime,duration, andwindowAttributionwhen 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:
- Detect INP regression via
eventobserver orweb-vitalson the affected route. - Filter LoAF entries whose
startTimeoverlaps the slow interaction'sprocessingStart–processingEndwindow. - Rank scripts by
blockingDuration; open the named bundle at the attributed function. - Fix: yield with
scheduler.yield()where supported, chunk work, defer torequestIdleCallback, or move CPU to a worker. - 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
eventobserver for INP with route and release tags. - New
long-animation-frameobserver alongside existinglongtask— same beacon pipeline. - Dashboard column: top script URL by summed
blockingDurationper route per week.
Findings
- Autocomplete normalization — 140 ms blocking on
inputevents; fixed with 150 ms debounce and Web Worker normalization for bulk paste. - Pay-button click handler — LoAF showed 60 ms in
JSON.stringifyon 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
evententries; 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 totalduration; 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
longtaskfallback and lab profiling for other engines.
Production checklist
- Feature-detect
long-animation-frameinPerformanceObserver.supportedEntryTypes. - Register buffered LoAF observer on checkout, editor, and dashboard routes.
- Beacon top three scripts by
blockingDurationper slow interaction. - Correlate LoAF timestamps with
evententry processing windows. - Require
Timing-Allow-Originon first-party and contracted third-party scripts. - Alert when any single script exceeds 100 ms blocking on a critical route.
- Keep
longtaskobserver 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
- Performance Observer API explained — buffered observers, entry types, and RUM
- Core Web Vitals explained — LCP, INP, and CLS thresholds
- Web Workers explained — moving CPU work off the main thread
- Code splitting explained — deferring heavy bundles from the critical path