Skip to the content

Long Animation Frames

Records each frame of 50 ms or more from the Long Animation Frames API (LoAF): its duration, the blocking duration of its long tasks, and script attribution in Chromium.

LongAnimationFrameMonitor records each long animation frame (LoAF) that the browser reports. The browser reports each frame of 50 ms or more, with the scripts of the frame. Only Chromium has this API.

What it measures

The monitor records two values for each long frame:

  • The blocking duration: the part of the frame that blocked input. For each long task in the frame, the time over 50 ms counts, and the render phase counts as part of the task (browser support).
  • The duration: the full time of the frame, also the time that did not block input.

The monitor answers this question: which frames blocked the page, and which script caused it? It gives the attribution that the timer monitors cannot give. Use it in Chromium together with DriftLag and the worker monitor, which work in all browsers.

How it works

The monitor makes a PerformanceObserver for the entry type long-animation-frame, with buffered: true. Thus it also gets the frames from before its start, as far as the buffer of the browser keeps them. For each entry, the monitor gives a report with these fields:

FieldValue
blockingDuration, duration, startTimeFrom the entry.
renderDurationFrom renderStart to the end of the frame. It is 0 for a frame that did not render (renderStart is 0).
scriptCountThe number of scripts in scripts.
hasForceLayoutTrue if a script has a forcedStyleAndLayoutDuration of more than 0.
topScriptThe script with the longest duration: invoker, invokerType, sourceURL, sourceFunctionName, duration.

The factory records blockingDuration and duration for each frame. For a frame that blocks for 150 ms or more, it also sends a lag.long_animation_frame event with the longest script. It sends at most 10 events in each period of 60 s, and it removes the query string and the fragment from the script URL.

The invoker can also contain a URL, and the factory removes its query string and its fragment too. For classic-script and module-script, the invoker is the URL of the script. For an inline script, it is the URL of the document, which can contain tokens. For an event listener on an element without an ID, the invoker contains the src attribute, for example IMG[src=https://cdn.example/a.png].onload.

When the browser does not list long-animation-frame in PerformanceObserver.supportedEntryTypes, the monitor logs a warning and gets no entries.

Browser support

BrowserLong Animation Frames
Chromium123 (March 2024). paintTime and presentationTime from 145.
FirefoxNot available. Mozilla has a positive position, and bug 1348405 is open.
SafariNot available. WebKit bug 270913 is open.

The API is not available in workers. The specification is a W3C First Public Working Draft of 28 April 2026. Chromium approved the style and layout durations (styleDuration, layoutDuration and their forced versions) to ship in Chrome 157 (browser support).

These facts of the API limit the attribution:

  • The browser reports only frames of 50 ms or more. A script is in scripts only if it took more than 5 ms.
  • The browser keeps a buffer of 200 entries.
  • The browser gives no script attribution for cross-origin iframes, workers, service workers or extensions.
  • For a cross-origin script without CORS, the browser gives only sourceURL, and sourceFunctionName is empty.

Measurement validity

The monitor does not pause while the page is hidden, and it uses no SampleValidator. It records each frame that the browser delivers. The monitor uses no timers, thus the timer throttling of hidden pages does not change it.

Metrics

MetricKindUnitAttributesDescription
lag_loaf_blocking_histogramHistogrammsNoneThe blocking duration of each long animation frame.
lag_loaf_duration_histogramHistogrammsNoneThe total duration of each long animation frame.

The long frame event

lag.long_animation_frame has these attributes:

AttributeValue
duration_msThe duration of the frame.
blocking_duration_msThe blocking duration of the frame.
script.invokerThe invoker of the longest script, for example BUTTON#buy.onclick. A URL in the invoker has no query string and no fragment.
script.invoker_typeFor example event-listener or user-callback.
script.source_urlThe URL of the script, without the query string and the fragment.
script.duration_msThe duration of the longest script.

A frame without script attribution gives only duration_ms and blocking_duration_ms. setupAllMonitors() adds lag.page_view.id to each event.

Configuration

function createInstrumentedLoaf(
    deps : CoreDeps & ObserverDeps & Partial<EventDeps> & Partial<AbsoluteClockDeps> & Partial<PerformanceDeps>,
) : MonitorHandle<LongAnimationFrameMonitor>;

The factory uses CoreDeps (logger, clock, meter) and ObserverDeps (PerformanceObserver). EventDeps (events) is optional: without it, the factory sends no events. AbsoluteClockDeps (absoluteClock) and PerformanceDeps (performance) are optional: they give the events their times. Without both, the events get the time of the call. The time of an event is the start of its frame. The attribution threshold (150 ms) and the event limit (10 each minute) are constants of the factory.

import { metrics } from "@opentelemetry/api";
import { logs } from "@opentelemetry/api-logs";
import { createBrowserDeps, createInstrumentedLoaf, createOtelEventSink } from "@mark1russell7/lag";

const deps = createBrowserDeps(window, {
    logger : console,
    meter : metrics.getMeter("lag"),
    events : createOtelEventSink(logs.getLogger("lag-events")),
});
if (deps.PerformanceObserver) {
    const loaf = createInstrumentedLoaf({ ...deps, PerformanceObserver : deps.PerformanceObserver });
    loaf.stop();
}

Cost

  • Observers: one PerformanceObserver. No timers.
  • Records: two histogram values for each long frame, and only for frames of 50 ms or more.
  • Events: at most 10 each minute.

Limits

  • Only Chromium. Firefox and Safari have no long frame entries. There, use the timer monitors and the worker monitor.
  • The longest script only. The event names the script with the longest duration. The other scripts of the frame are not in the event.
  • Events are limited. After 10 events in a period of 60 s, the factory sends no more events in that period. The histograms still record each frame.
  • The attribution gaps of the API. Scripts of 5 ms or less, cross-origin iframes, workers and extensions give no attribution.
  • Duration is not blocking. duration also contains time that did not block input. Use blockingDuration for the effect on input.

Tests

Unit tests:

  • LongAnimationFrameMonitor.test.ts: the monitor reports the blocking duration and the duration of the entry. It calculates the render duration from renderStart, and 0 for a frame that did not render. It finds the forced layout of the scripts.
  • ObserverMonitor.test.ts: the observer uses buffered: true. The monitor skips a type that is not in supportedEntryTypes, and it logs a warning when observe() fails. An error in one entry does not stop the other entries.
  • instrumented/loaf.test.ts: the factory sends an event only for a frame that blocks for 150 ms or more, and at most 10 events each minute. The script URL and a URL in the invoker (classic-script, module-script, and [src=…] of an event listener) have no query string and no fragment.
  • setup-all-monitors.test.ts: the event has the blocking duration, the invoker, the script URL without its query string, and the page-view ID.

Browser test (Vitest browser mode with Playwright, in Chromium, Firefox, WebKit and Chrome):

  • lag-monitors.test.ts (only where the browser has Long Animation Frames: Chromium and Chrome): a 150 ms block occurs in an animation frame. Then the blocking durations of the monitor are equal to the blocking durations of a second, raw observer.

Source

lag: Main-thread responsiveness monitoring for browser apps, exported as OpenTelemetry metrics.

To change a page, edit its file in packages/site/content/. The writing style guide tells you how.

An AI model (Claude, from Anthropic) wrote most of the text and the code of this site and of the library, under the direction of the author. The tests and the STE linter examine them. The writing standard gives the reason for this note.