docs.rodeo

MDN Web Docs mirror

Long animation frame timing

{{DefaultAPISidebar("Performance API")}} 

Long animation frames (LoAFs) can impact the user experience of a website. They can cause slow user interface (UI) updates, resulting in seemingly unresponsive controls and janky (or non-smooth) animated effects and scrolling, leading to user frustration. The Long Animation Frames API allows developers to get information about the long animation frames and better understand their root causes. This article shows how to use the Long Animation Frames API.

What is a long animation frame?

A long animation frame — or LoAF — is a rendering update that is delayed beyond 50ms.

Good responsiveness means that a page responds quickly to interactions. This involves painting any updates needed by the user in a timely manner and avoiding anything that could block these updates. Google’s Interaction to Next Paint (INP) metric, for example, recommends that a website should respond to page interactions (such as clicks or key presses) within 200ms.

For smooth animations, updates need to be fast — for an animation to run at a smooth 60 frames per second, each animation frame should render within around 16ms (1000/60).

Observing long animation frames

To obtain information on LoAFs and pinpoint troublemakers, you can observe performance timeline entries with an {{domxref("PerformanceEntry.entryType", "entryType")}}  of "long-animation-frame" using a standard {{domxref("PerformanceObserver")}} :

const observer = new PerformanceObserver((list) => {
  console.log(list.getEntries());
});

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

Previous long animation frames can also be queried, using a method such as {{domxref("Performance.getEntriesByType()")}} :

const loafs = performance.getEntriesByType("long-animation-frame");

Be aware, however, that the maximum buffer size for "long-animation-frame" entry types is 200, after which new entries are dropped, so using the PerformanceObserver approach is recommended.

Examining "long-animation-frame" entries

Performance timeline entries returned with a type of "long-animation-frame" are represented by {{domxref("PerformanceLongAnimationFrameTiming")}}  objects. This object has a {{domxref("PerformanceLongAnimationFrameTiming.scripts", "scripts")}}  property containing an array of {{domxref("PerformanceScriptTiming")}}  objects, each one of which contains information about a script that contributed to the long animation frame.

The following is a JSON representation of a complete "long-animation-frame" performance entry example, containing a single script:

{
  "blockingDuration": 0,
  "duration": 60,
  "entryType": "long-animation-frame",
  "firstUIEventTimestamp": 11801.099999999627,
  "name": "long-animation-frame",
  "renderStart": 11858.800000000745,
  "scripts": [
    {
      "duration": 45,
      "entryType": "script",
      "executionStart": 11803.199999999255,
      "forcedStyleAndLayoutDuration": 0,
      "invoker": "DOMWindow.onclick",
      "invokerType": "event-listener",
      "name": "script",
      "pauseDuration": 0,
      "sourceURL": "https://web.dev/js/index-ffde4443.js",
      "sourceFunctionName": "myClickHandler",
      "sourceCharPosition": 17796,
      "startTime": 11803.199999999255,
      "window": [Window object],
      "windowAttribution": "self"
    }
  ],
  "startTime": 11802.400000000373,
  "styleAndLayoutStart": 11858.800000000745
}

Beyond the standard data returned by a {{domxref("PerformanceEntry")}}  entry, this contains the following noteworthy items:

Calculating timestamps

The timestamps provided in the {{domxref("PerformanceLongAnimationFrameTiming")}}  class allow several further useful timings to be calculated for the long animation frame:

Timing Calculation
Start time startTime
End time startTime + duration
Work duration renderStart ? renderStart - startTime : duration
Render duration renderStart ? (startTime + duration) - renderStart : 0
Render: Pre-layout duration styleAndLayoutStart ? styleAndLayoutStart - renderStart : 0
Render: Style and Layout duration styleAndLayoutStart ? (startTime + duration) - styleAndLayoutStart : 0

Examples

Long Animation Frames API feature detection

You can test whether the Long Animation Frames API is supported using {{domxref("PerformanceObserver.supportedEntryTypes_static", "PerformanceObserver.supportedEntryTypes")}} :

if (PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {
  // Monitor LoAFs
}

Reporting LoAFs above a certain threshold

While LoAF thresholds are fixed at 50ms, this may lead to a large volume of reports when you first start performance optimization work. Initially, you may want to report LoAFs at a higher threshold value and gradually decrease the threshold as you improve the site and remove the worst LoAFs. The following code could be used to capture LoAFs above a specific threshold for further analysis (for example, by sending them back to an analytics endpoint):

const REPORTING_THRESHOLD_MS = 150;

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > REPORTING_THRESHOLD_MS) {
      // Example here logs to console; real code could send to analytics endpoint
      console.log(entry);
    }
  }
});

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

Long animation frame entries can be quite large; therefore, think carefully about what data from each entry should be sent to analytics. For example, the summary times of the entries and the script URLs might be enough for what you need.

Observing the longest animation frames

You may wish to only collect data on the longest animation frames (say the top 5 or 10), to reduce the volume of data that needs to be collected. This could be handled as follows:

MAX_LOAFS_TO_CONSIDER = 10;
let longestBlockingLoAFs = [];

const observer = new PerformanceObserver((list) => {
  longestBlockingLoAFs = longestBlockingLoAFs
    .concat(list.getEntries())
    .sort((a, b) => b.blockingDuration - a.blockingDuration)
    .slice(0, MAX_LOAFS_TO_CONSIDER);
});
observer.observe({ type: "long-animation-frame", buffered: true });

// Report data on visibilitychange event
document.addEventListener("visibilitychange", () => {
  // Example here logs to console; real code could send to analytics endpoint
  console.log(longestBlockingLoAFs);
});

Reporting long animation frames with interactions

Another useful technique is to send the largest LoAF entries where an interaction occurred during the frame, which can be detected by the presence of a {{domxref("PerformanceLongAnimationFrameTiming.firstUIEventTimestamp", "firstUIEventTimestamp")}}  value.

The following code logs all LoAF entries greater than 150ms where an interaction occurred during the frame. You could choose a higher or lower value depending on your needs.

const REPORTING_THRESHOLD_MS = 150;

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (
      entry.duration > REPORTING_THRESHOLD_MS &&
      entry.firstUIEventTimestamp > 0
    ) {
      // Example here logs to console; real code could send to analytics endpoint
      console.log(entry);
    }
  }
});

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

Identifying common script patterns in long animation frames

An alternative strategy is to look at which scripts appear most often in LoAF entries. Data could be reported at the level of a script and/or character position to identify the most problematic scripts. This is useful in cases where themes or plugins causing performance issues are used across multiple sites.

The execution times of common scripts (or third-party origins) in LoAFs could be summed up and reported back to identify common contributors to LoAFs across a site or a collection of sites.

For example, to group scripts by URL and show total duration:

const observer = new PerformanceObserver((list) => {
  const allScripts = list.getEntries().flatMap((entry) => entry.scripts);
  const scriptSource = [
    ...new Set(allScripts.map((script) => script.sourceURL)),
  ];
  const scriptsBySource = scriptSource.map((sourceURL) => [
    sourceURL,
    allScripts.filter((script) => script.sourceURL === sourceURL),
  ]);
  const processedScripts = scriptsBySource.map(([sourceURL, scripts]) => ({
    sourceURL,
    count: scripts.length,
    totalDuration: scripts.reduce(
      (subtotal, script) => subtotal + script.duration,
      0,
    ),
  }));
  processedScripts.sort((a, b) => b.totalDuration - a.totalDuration);
  // Example here logs to console; real code could send to analytics endpoint
  console.table(processedScripts);
});

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

Comparing with the Long Tasks API

The Long Animation Frames API was preceded by the Long Tasks API (see {{domxref("PerformanceLongTaskTiming")}} ). Both the APIs have a similar purpose and usage — exposing information about long tasks that block the main thread for 50ms or more.

Cutting down the number of long tasks that occur on your website is useful because long tasks can cause responsiveness issues. For example, if a user clicks a button while the main thread is dealing with a long task, the UI response to the click will be delayed until the long task is completed. Conventional wisdom is to break up long tasks into multiple smaller tasks so that important interactions can be handled in between.

However, the Long Tasks API has its limitations:

See also

In this article

View on MDN