ZyVOP Logo
Content That Connects
SeriesAI NewsLeaderboardWrite for Us
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • API Documentation
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeThe Long Animation Frames API: Find What Actually Broke Your INP

The Long Animation Frames API: Find What Actually Broke Your INP

Danny Holloran
Danny HolloranSenior Developer
August 10, 2026
3 min read
The Long Animation Frames API: Find What Actually Broke Your INP
#web-performance#web-apis#JavaScript#performance
👍1

Your real user monitoring dashboard says the 75th percentile INP on your product page is 412ms. You open DevTools, click the same button forty times, and every interaction comes back at 60ms. Nothing reproduces. So you start guessing: maybe it's the analytics tag, maybe it's the third-party chat widget, maybe it's that one useEffect that everybody is afraid to touch.

The Long Tasks API was supposed to help here, and it mostly didn't. It would tell you a task ran for 210ms and attribute it to "self" or an iframe container, which is roughly as actionable as a smoke alarm that only reports "somewhere in the house." The Long Animation Frames API is the replacement, and it does the thing you actually wanted all along: it hands you a script URL, a function name, and a character offset.

A frame is a better unit of measurement than a task

Slow interactions are rarely one fat task. They're usually five medium tasks, a ResizeObserver callback, and a style recalculation, all landing between two paints. Long Tasks only saw the fat ones, and it stopped measuring before rendering even started.

LoAF widens the window. It reports on any animation frame whose total work exceeded 50ms, covering everything from the first task in that frame through style, layout, and paint. Entries arrive with entryType of long-animation-frame:

const observer = new PerformanceObserver((list) => {
  for (const frame of list.getEntries()) {
    console.log({
      duration: frame.duration,
      blockingDuration: frame.blockingDuration,
      scriptTime: frame.renderStart ? frame.renderStart - frame.startTime : 0,
      styleAndLayout: frame.styleAndLayoutStart ? frame.paintTime - frame.styleAndLayoutStart : 0,
    });
  }
});
observer.observe({ type: "long-animation-frame", buffered: true });

Four timestamps do most of the work. startTime is when the frame began, renderStart is when script handed off to the rendering pipeline, styleAndLayoutStart is where recalc and layout begin, and paintTime is where it ends. Subtracting them tells you whether you have a JavaScript problem or a layout problem, which are fixed in completely different ways.

The one field to alert on is blockingDuration. It sums the over-50ms portion of every long task in the frame plus the rendering time, so it approximates how long the main thread was genuinely unavailable to a user. A 300ms frame with a 20ms blockingDuration is fine. A 120ms frame with a 90ms blockingDuration is not.

Script attribution is the whole point

Every LoAF entry carries a scripts array of PerformanceScriptTiming objects, and this is where the guessing stops:

const worst = frame.scripts.sort((a, b) => b.duration - a.duration)[0];
console.log({
  url: worst.sourceURL, // https://cdn.example.com/widget.js
  fn: worst.sourceFunctionName, // handleScroll
  char: worst.sourceCharPosition, // 18422
  invoker: worst.invoker, // window.onscroll
  invokerType: worst.invokerType, // event-listener
  duration: worst.duration,
  forcedLayout: worst.forcedStyleAndLayoutDuration,
  paused: worst.pauseDuration,
});

invokerType tells you how the script got run: event-listener, user-callback, resolve-promise, module-script, classic-script. That distinction matters, because a slow event-listener is your code to fix and a slow classic-script during load is usually a tag manager you need to defer.

forcedStyleAndLayoutDuration is quietly the best field in the API. If it's a meaningful chunk of duration, you have layout thrashing: something read offsetHeight or getBoundingClientRect() inside a write loop and forced synchronous layout. You now know which function, in which file, at which character.

The caveat worth knowing before you build dashboards on this: attribution only covers same-origin main-thread scripts. Cross-origin iframes, web workers, service workers, and browser extensions can lengthen a frame without ever showing up in scripts. When you see a big duration with a nearly empty scripts array, that absence is itself the signal.

Tie it to the interaction, not just the clock

A stream of slow frames is noise until you connect it to a specific bad interaction. The easiest path is web-vitals with the attribution build, which already correlates LoAF entries with the INP interaction for you:

import { onINP } from "web-vitals/attribution";

onINP(({ value, attribution }) => {
  if (value < 200) return;
  const culprit = attribution.longAnimationFrameEntries
    .flatMap((frame) => frame.scripts)
    .sort((a, b) => b.duration - a.duration)[0];

  beacon("/rum", {
    inp: value,
    target: attribution.interactionTarget,
    inputDelay: attribution.inputDelay,
    processing: attribution.processingDuration,
    presentation: attribution.presentationDelay,
    script: culprit && `${culprit.sourceURL}:${culprit.sourceFunctionName}`,
  });
});

Send the single worst script, not the whole entry. A busy page can produce dozens of LoAFs in a session and serializing all of them will cost you more than the insight is worth.

LoAF shipped in Chrome 123 after an origin trial, and it's still Chromium-only. That sounds limiting until you remember that Chrome is also where your Core Web Vitals field data comes from, so the coverage gap and the metric you're optimizing line up almost exactly.

Start by logging blockingDuration and the top script for interactions over 200ms, then read the Chrome documentation and the spec once you know which part of your frame is actually on fire.

Danny Holloran

Danny Holloran

Senior Developer

Senior Frontend & Fullstack Developer with 14+ years building performant, scalable web applications. Passionate about architecture, mentorship, and finding the right tool for the job.

Comments (0)

Login to post a comment.

Related Posts

Angular 22: The End of Boilerplate and the Consolidation of the Reactive Era

If you have been following the evolution of Google's framework over the last few years, you know it has been undergoing a silent reconstruction — piece by piece...

Read article

Tiled Rasterization for Large DOM Captures

SnapDOM moves cropping before image decode, turning huge DOM captures into bounded canvas tiles without sacrificing resolution or allocating one enormous bitmap.

Read article

Trusted Types Is Baseline: DOM XSS Is Now a Type Error

Firefox 148 shipped Trusted Types in February 2026, making it Baseline. Here's how to turn every dangerous innerHTML assignment in your app into a TypeError you can actually catch.

Read article

Code-Splitting Is a Boundary Decision, Not a Bundle Trick

The command palette article closed by promising the last piece of this look-and-feel stretch, and it's the one that looks the most like a solved problem: code-s...

Read article

Misusing React Context, Then Blaming React Context

Stop blaming React Context for unnecessary re-renders. Discover how React composition affects rendering performance and how to build efficient Context providers.

Read article