DEV Community

Pop Watch
Pop Watch

Posted on

Debug UI Jank with the Long Animation Frames API

A page can feel slow even when its network waterfall looks fine. A click appears to do nothing, a scroll hitches, or an animation skips. The difficult part is often not proving that the main thread was busy; it is identifying what occupied the frame that prevented the next paint.

The Long Animation Frames API (LoAF) gives browser code a structured way to observe animation frames that take 50 ms or longer. Unlike a task-only view, it reports a frame as a unit: script execution, rendering work, and the time during which the frame blocked responsiveness. That makes it useful for debugging—provided we treat it as evidence, not as a synthetic performance score.

This article builds a small, privacy-conscious LoAF probe, explains how to interpret it, and shows how to turn one bad frame into a reproducible DevTools investigation.

What LoAF measures

A LoAF entry represents one animation frame whose duration is at least 50 ms. It exposes a duration, a blockingDuration, a rendering start time, and a list of scripts that ran in that frame. Script records can include the script's source URL, function name, invoker type, and timing details.

That frame-level boundary matters. A single user interaction can be delayed by a handler, framework rendering, style calculation, layout, paint preparation, or several of those in sequence. Looking only for a long JavaScript task can hide the rest of the frame. LoAF does not replace a trace, but it is a compact signal that tells you which timeframe deserves a trace.

LoAF is related to responsiveness, not identical to Interaction to Next Paint (INP). INP is an interaction-centric field metric: it follows an interaction until the next paint. LoAF is frame-centric diagnostic data. A long frame can occur without a user interaction, and a poor interaction can contain work that LoAF does not fully explain. Keep those two questions separate:

  • How did users experience the interaction? Measure INP.
  • What occupied a suspicious frame? Inspect LoAF and then a performance trace.

Start with feature detection

The API is not a portable analytics primitive. Support is browser-dependent, and the standard remains a W3C Working Draft. Do not make application behavior depend on it. Use feature detection and preserve a no-op path:

function supportsLoAF() {
  return PerformanceObserver.supportedEntryTypes?.includes(
    "long-animation-frame",
  );
}

if (supportsLoAF()) {
  console.info("LoAF diagnostics are available");
}
Enter fullscreen mode Exit fullscreen mode

Checking supportedEntryTypes is preferable to assuming that a browser version supports the entry type. It also keeps the diagnostic code safe to ship behind a development flag or an explicit opt-in.

A small local diagnostic probe

Here is a deliberately modest observer. It prints a compact summary and limits the number of script records so a bad page does not generate an unusable console dump.

function observeLongAnimationFrames({ maxScripts = 3 } = {}) {
  if (!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) {
    return () => {};
  }

  const observer = new PerformanceObserver((list) => {
    for (const frame of list.getEntries()) {
      const scripts = (frame.scripts ?? []).slice(0, maxScripts).map((script) => ({
        source: script.sourceURL || "inline or unavailable",
        functionName: script.functionName || "anonymous",
        invoker: script.invoker || "unknown",
        duration: Math.round(script.duration),
      }));

      console.table([{
        start: Math.round(frame.startTime),
        duration: Math.round(frame.duration),
        blocking: Math.round(frame.blockingDuration),
        renderStart: Math.round(frame.renderStart),
        scripts,
      }]);
    }
  });

  observer.observe({ type: "long-animation-frame", buffered: true });
  return () => observer.disconnect();
}

const stopLoAF = observeLongAnimationFrames();
// Call stopLoAF() when the diagnostic session ends.
Enter fullscreen mode Exit fullscreen mode

Run this locally or in a controlled test environment, then reproduce one concrete action: opening a menu, filtering a list, dragging a map, or submitting a form. Avoid “click around for a minute” as a test plan. A named action gives the timestamp and frame data a useful boundary.

The buffered option asks the observer to receive relevant entries already recorded before the observer started. That is useful when the slow work happens during startup, but it is also a reason to attach the observer early and keep the session short.

Read the record without over-claiming

Suppose a frame reports a duration near 180 ms and a large blocking duration. Do not immediately rewrite every function listed in scripts. First ask four narrower questions:

  1. Does the frame coincide with the action you reproduced? Correlation is not causation. Record the action time or add a temporary console marker immediately before it.
  2. Is script time dominant? If script entries account for little of the frame, inspect rendering work in a DevTools trace rather than optimizing a random callback.
  3. Is the same source and function repeated? Repetition across multiple reproductions is stronger evidence than one outlier.
  4. Is the cost first-party, third-party, or browser/framework work? The remedy and the ownership differ.

The script list is a lead, not a flame chart. A function can be present because it triggered later work; its own duration may not be the entire cost of the frame. This is why the next step is a targeted trace.

Turn a LoAF signal into a trace

Use the observer to find a repeatable slow action, then capture that same action in Chrome DevTools' Performance panel. Align the trace with the LoAF timestamp and inspect the long frame. Look for a concrete mechanism:

  • synchronous JavaScript that can be split, deferred, or moved off the main thread;
  • repeated layout reads and writes that force layout between updates;
  • expensive DOM updates that can be batched or narrowed;
  • a third-party callback running during an interaction;
  • too much work scheduled in one animation frame.

Only then choose an intervention. For example, if a click handler computes a large filter synchronously, moving the computation to a worker may help. If the trace instead shows repeated layout after DOM writes, a worker will not fix the rendering dependency. LoAF is valuable because it prevents this category error early.

Respect privacy and operational boundaries

Performance diagnostics can accidentally become telemetry. The script sourceURL may expose application paths, query strings, or third-party origins. Function names and timestamps can also reveal implementation details. Treat LoAF records as debug data.

A conservative production policy is:

  • keep full records in local development or an authenticated internal diagnostic tool;
  • if collecting aggregated field signals, minimize them to coarse durations and a first-party category—not raw URLs, function names, page content, or interaction text;
  • sample deliberately, set a retention limit, document the purpose, and honor your product's consent and privacy requirements;
  • never use this API to fingerprint users or infer behavior from timing traces.

The API is also not a substitute for accessibility testing, real-device testing, or user-facing metrics. A 50 ms frame threshold is an engineering signal, not a promise that the page feels good.

A practical debugging loop

  1. Add the observer behind a debug flag.
  2. Reproduce one named interaction three times.
  3. Group the frames by action, source, and rough duration.
  4. Capture one matching DevTools trace.
  5. Fix the dominant mechanism, then repeat the same interaction.
  6. Remove or disable the probe when the investigation ends.

This loop is intentionally boring. It avoids the common failure mode of treating every performance entry as an optimization mandate. Good debugging begins with a precise observation, confirms the mechanism in a trace, and changes only the work that is actually on the critical path.

Sources

Top comments (0)