Long Animation Frames API

Noam Rosenthal
Noam Rosenthal

The Long Animation Frames API (LoAF-pronounced Lo-Af) is an update to the Long Tasks API to provide a better understanding of slow user interface (UI) updates. This can be useful to identify slow animation frames which are likely to affect the Interaction to Next Paint (INP) Core Web Vital metric which measures responsiveness, or to identify other UI jank which affects smoothness.

Status of the API

Browser Support

  • Chrome: 123.
  • Edge: 123.
  • Firefox: not supported.
  • Safari: not supported.

Source

Following an origin trial from Chrome 116 to Chrome 122, the LoAF API has shipped from Chrome 123.

Background: the Long Tasks API

Browser Support

  • Chrome: 58.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

The Long Animation Frames API is an alternative to the Long Tasks API which has been available in Chrome for some time now (since Chrome 58). As its name suggests, the Long Task API lets you monitor for long tasks, which are tasks that occupy the main thread for 50 milliseconds or longer. Long tasks can be monitored using the PerformanceLongTaskTiming interface, with a PeformanceObserver:

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

observer.observe({ type: 'longtask', buffered: true });

Long tasks are likely to cause responsiveness issues. If a user tries to interact with a page—for example, click a button, or open a menu—but the main thread is already dealing with a long task, then the user's interaction is delayed waiting for that task to be completed.

To improve responsiveness, it is often advised to break up long tasks. If each long task is instead broken up into a series of multiple, smaller tasks, it may allow more important tasks to be executed in between them to avoid significant delays in responding to interactions.

So when trying to improve responsiveness, the first effort is often to run a performance trace and look at long tasks. This could be through a lab-based auditing tool like Lighthouse (which has an Avoid long main-thread tasks audit), or by looking at long tasks in Chrome DevTools.

Lab-based testing is often a poor starting place for identifying responsiveness issues, as these tools may not include interactions—when they do, they are a small subset of likely interactions. Ideally, you would measure causes of slow interactions in the field.

Shortcomings of the Long Tasks API

Measuring long tasks in the field using a Performance Observer is only somewhat useful. In reality, it doesn't give that much information beyond the fact that a long task happened, and how long it took.

Real User Monitoring (RUM) tools often use this to trend the number or duration of long tasks or identifying which pages they happen on—but without the underlying details of what caused the long task, this is only of limited use. The Long Tasks API only has a basic attribution model, which at best only tells you the container the long task happened in (the top-level document or an <iframe>), but not the script or function which called it, as shown by a typical entry:

{
  "name": "unknown",
  "entryType": "longtask",
  "startTime": 31.799999997019768,
  "duration": 136,
  "attribution": [
    {
      "name": "unknown",
      "entryType": "taskattribution",
      "startTime": 0,
      "duration": 0,
      "containerType": "window",
      "containerSrc": "",
      "containerId": "",
      "containerName": ""
    }
  ]
}

The Long Tasks API is also an incomplete view, since it may also exclude some important tasks. Some updates—like rendering—happen in separate tasks that ideally should be included together with the preceding execution that caused that update to accurately measure the "total work" for that interaction. For more details of the limitations of relying on tasks, see the "Where long tasks fall short" section of the explainer.

The final issue is that measuring long tasks only reports on individual tasks that take longer than the 50 millisecond limit. An animation frame could be made up of several tasks smaller than this 50 millisecond limit, yet collectively still block the browser's ability to render.

The Long Animation Frames API

Browser Support

  • Chrome: 123.
  • Edge: 123.
  • Firefox: not supported.
  • Safari: not supported.

Source

The Long Animation Frames API (LoAF) is a new API that seeks to address some of the shortcomings of the Long Tasks API to enable developers to get more actionable insights to help address responsiveness problems and improve INP.

Good responsiveness means that a page responds quickly to interactions made with it. That involves being able to paint any updates needed by the user in a timely manner, and avoiding blocking these updates from happening. For INP, it is recommended to respond in 200 milliseconds or less, but for other updates (for example, animations) even that may be too long.

The Long Animation Frames API is an alternative approach to measuring blocking work. Rather than measuring the individual tasks, the Long Animation Frames API—as its name suggests—measures long animation frames. A long animation frame is when a rendering update is delayed beyond 50 milliseconds (the same as the threshold for the Long Tasks API).

Long animation frames can be observed in a similar way as long tasks with a PerformanceObserver, but looking at long-animation-frame type instead:

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 from the Performance Timeline like so:

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

However, there is a maxBufferSize for performance entries after which newer entries are dropped, so the PerformanceObserver approach is the recommended approach. The long-animation-frame buffer size is set to 200, the same as for long-tasks.

Advantages of looking at frames instead of tasks

The key advantage of looking at this from a frame perspective rather than a tasks perspective, is that a long animation can be made up of any number of tasks that cumulatively resulted in a long animation frame. This addresses the final point mentioned previously, where the sum of many smaller, render-blocking tasks before an animation frame may not be surfaced by the Long Tasks API.

A further advantage of this alternative view on long tasks, is the ability to provide timing breakdowns of the entire frame. Rather than just including a startTime and a duration, like the Long Tasks API, LoAF includes a much more detailed breakdown of the various parts of the frame duration including:

  • startTime: the start time of the long animation frame relative to the navigation start time.
  • duration: the duration of the long animation frame (not including presentation time).
  • renderStart: the start time of the rendering cycle, which includes requestAnimationFrame callbacks, style and layout calculation, resize observer and intersection observer callbacks.
  • styleAndLayoutStart: the beginning of the time period spent in style and layout calculations.
  • firstUIEventTimestamp: the time of the first UI event (mouse/keyboard and so on) to be handled during the course of this frame.
  • blockingDuration: the duration in milliseconds for which the animation frame was being blocked.

These timestamps allow the long animation frame to be divided into timings:

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

For more details on these individual timings, refer to the explainer, which provides fine-grained detail as to which activity is contributing to a long animation frame.

Better attribution

The long-animation-frame entry type includes better attribution data of each script that contributed to a long animation frame.

Similar to the Long Tasks API, this will be provided in an array of attribution entries, each of which details:

  • A name and EntryType both will return script.
  • A meaningful invoker, indicating how the script was called (for example, 'IMG#id.onload', 'Window.requestAnimationFrame', or 'Response.json.then').
  • The invokerType of the script entry point:
    • user-callback: A known callback registered from a web platform API (for example, setTimeout, requestAnimationFrame).
    • event-listener: A listener to a platform event (for example, click, load, keyup).
    • resolve-promise: Handler of a platform promise (for example, fetch(). Note that in the case of promises, all the handlers of the same promises are mixed together as one "script").
    • reject-promise: As per resolve-promise, but for the rejection.
    • classic-script: Script evaluation (for example, <script> or import())
    • module-script: Same as classic-script, but for module scripts.
  • Separate timing data for that script:
    • startTime: Time the entry function was invoked.
    • duration: The duration between startTime and when the subsequent microtask queue has finished processing.
    • executionStart: The time after compilation.
    • forcedStyleAndLayoutDuration: The total time spent processing forced layout and style inside this function (see thrashing).
    • pauseDuration: Total time spent in "pausing" synchronous operations (alert, synchronous XHR).
  • Script source details:
    • sourceURL: The script resource name where available (or empty if not found).
    • sourceFunctionName: The script function name where available (or empty if not found).
    • sourceCharPosition: The script character position where available (or -1 if not found).
  • windowAttribution: The container (the top-level document, or an <iframe>) the long animation frame occurred in.
  • window: A reference to the same-origin window.

Where provided, the source entries allows developers to know exactly how each script in the long animation frame was called, down to the character position in the calling script. This gives the exact location in a JavaScript resource that resulted in the long animation frame.

Example of a long-animation-frame performance entry

A complete long-animation-frame performance entry example, containing a single script, is:

{
  "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
}

As can be seen, this gives an unprecedented amount of data for websites to be able to understand the cause of laggy rendering updates.

Use the Long Animation Frames API in the field

Tools like Chrome DevTools and Lighthouse—while useful for discovering and reproducing issues—are lab tools that may miss important aspects of the user experience that only field data can provide.

The Long Animation Frames API is designed to be used in the field to gather important contextual data for user interactions that the Long Tasks API couldn't. This can help you to identify and reproduce issues with interactivity that you might not have otherwise discovered.

Feature detecting Long Animation Frames API support

You can use the following code to test if the API is supported:

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

The most obvious use case for the Long Animation Frames API is to to help diagnose and fix Interaction to Next Paint (INP) issues, and that was one of the key reasons the Chrome team developed this API. A good INP is where all interactions are responded to in 200 milliseconds or less from interaction until the frame is painted, and since the Long Animation Frames API measures all frames that take 50ms or more, most problematic INPs should include LoAF data to help you diagnose those interactions.

The "INP LoAF" is the LoAF which includes the INP interaction, as shown in the following diagram:

Examples of long animation frames on a page, with the INP LoAF highlighted.
A page may have many LoAFs, one of which is related to the INP interaction.

In some cases it's possible for an INP event to span two LoAFs—typically if the interaction happens after the frame has started the rendering part of the previous frame, and so the event handler it processed in the next frame:

Examples of long animation frames on a page, with the INP LoAF highlighted.
A page may have many LoAFs, one of which is related to the INP interaction.

It's even possible that it may span more than two LoAFs in some rare circumstances.

Recording the LoAF(s) data associated with the INP interaction lets you to get much more information about the INP interaction to help diagnose it. This is particularly helpful to understand input delay: as you can see what other scripts were running in that frame.

It can also be helpful to understand unexplained processing duration and presentation delay if your event handlers are not reproducing the values seen for those as other scripts may be running for your users which may not be included in your own testing.

There is no direct API to link an INP entry with its related LoAF entry or entries, though it is possible to do so in code by comparing the start and end times of each (see the WhyNp example script).

The web-vitals library includes all intersecting LoAFs in the longAnimationFramesEntries property of the INP attribution interface from v4.

Once you have linked the LoAF entry or entries, you can include information with INP attribution. The scripts object contains some of the most valuable information as it can show what else was running in those frames so beaconing back that data to your analytics service will allow you to understand more about why interactions were slow.

Reporting LoAFs for the INP interaction is a good way to find the what is the most pressing interactivity issues on your page. Each user may interact differently with your page and with enough volume of INP attribution data, a number of potential issues will be included in INP attribution data. This lets you to sort scripts by volume to see which scripts are correlating with slow INP.

Report more long animation data back to an analytics endpoint

One downside to only looking at the INP LoAF(s), is you may miss other potential areas for improvements that may cause future INP issues. This can lead to a feeling of chasing your tail where you fix an INP issue expecting to see a huge improvement, only to find the next slowest interaction is only a small amount better than that so your INP doesn't improve much.

So rather than shining a spotlight on only looking at the INP LoAF, you may want to consider all LoAFs across the page lifetime:

A page with many LoAFs, some of which happen during interactions even if not the INP interaction.
Looking at all LoAFs can help identify future INP problems.

However, each LoAF entry contains considerable data, so you will likely not want to beacon it all back. Instead you'll want to restrict your analysis to some LoAFs or some data.

Some suggested patterns include:

Which of these patterns works best for you, depends on how far along your optimization journey you are, and how common long animation frames are. For a site that has never optimized for responsiveness before, there may be many LoAFs do you may want to limit to just LoAFs with interactions, or set a high threshold, or only look at the worst ones. As you resolving your common responsiveness issues, you may expand this by not limiting to just interactions and by lowering thresholds, or look for particular patterns.

Observe long animation frames with interactions

To gain insights beyond just the INP long animation frame, you can look at all LoAFs with interactions (which can be detected by the presence of a firstUIEventTimestamp value).

This can also be an easier method of monitoring INP LoAFs rather than trying to correlate the two, which can be more complex. In most cases this will include the INP LoAF for a given visit, and in rare cases when it doesn't it still surfaces long interactions that are important to fix, as they may be the INP interaction for other users.

The following code logs all LoAF entries greater than 150 milliseconds where an interaction occurred during the frame. The 150 is chosen here because it is slightly less than the 200 millisecond "good" INP threshold. 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, but could also report back to analytics
        console.log(entry);
      }
    }
});
observer.observe({ type: 'long-animation-frame', buffered: true });

Observe animation frames longer than a certain threshold

Another strategy would be to monitor all LoAFs and beacon the ones greater than a certain threshold back to an analytics endpoint for later analysis:

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, but could also report back to analytics
      console.log(entry);
    }
  }
});
observer.observe({ type: 'long-animation-frame', buffered: true });

As the long animation frame entries can be quite large, developers should decide what data from the entry should be sent to analytics. For example, the summary times of the entry and perhaps the script names, or some other minimum set of other contextual data that may be deemed necessary.

Observe the worst long animation frames

Rather than having a set threshold, sites may want to collect data on the longest animation frame (or frames), to reduce the volume of data that needs to be beaconed. So no matter how many long animation frames a page experiences, only data for the worst one, five, or however many long animation frames absolutely necessary is beaconed back.

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 });

These strategies can also be combined—only look at the 10 worst LoAFs, with interactions, longer than 150 milliseconds.

At the appropriate time (ideally on the visibilitychange event) beacon back to analytics. For local testing you can use console.table periodically:

console.table(longestBlockingLoAFs);

Identify common patterns in long animation frames

An alternative strategy would be to look at common scripts appearing the most in long animation frame entries. Data could be reported back at a script and character position level to identify repeat offenders.

This may work particularly well for customizable platforms where themes or plugins causing performance issues could be identified across a number of sites.

The execution time of common scripts—or third-party origins—in long animation frames could be summed up and reported back to identify common contributors to long animation frames across a site or a collection of sites. For example to look at URLs:

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, but could also report back to analytics
  console.table(processedScripts);
});

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

And example of this output is:

(index) sourceURL count totalDuration
0 'https://example.consent.com/consent.js' 1 840
1 'https://example.com/js/analytics.js' 7 628
2 'https://example.chatapp.com/web-chat.js' 1 5

Use the Long Animation Frames API in tooling

The API also allows additional developer tooling for local debugging. While some tooling like Lighthouse and Chrome DevTools have been able to gather much of this data using lower-level tracing details, having this higher-level API could allow other tools to access this data.

Surface long animation frames data in DevTools

You can surface long animation frames in DevTools using the performance.measure() API, which are then displayed in the DevTools user timings track in performance traces to show where to focus your efforts for performance improvements:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    performance.measure('LoAF', {
      start: entry.startTime,
      end: entry.startTime + entry.duration,
    });
  }
});

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

Longer term, it will likely be incorporated into DevTools itself, but the previous code snippet allows it to be surfaced there in the meantime.

Use long animation frames data in other developer tooling

The Web Vitals extension has shown the value in logging summary debug information to diagnose performance issues.

It now also surfaces long animation frame data for each INP callback and each interaction:

Web Vitals Extension console logging.
Web Vitals Extension console logging surfaces LoAF data.

Use long animation frames data in automated testing tools

Similarly automated testing tools in CI/CD pipelines can surface details on potential performance issues by measuring long animation frames while running various test suites.

FAQ

Some of the frequently asked questions on this API include:

Why not just extend or iterate on the Long Tasks API?

This is an alternative look at reporting a similar—but ultimately different—measurement of potential responsiveness issues. It's important to ensure sites relying on the existing Long Tasks API continue to function to avoid disrupting existing use cases.

While the Long Tasks API may benefit from some of the features of LoAF (such as a better attribution model), we believe that focusing on frames rather than tasks offers many benefits that make this a fundamentally different API to the existing Long Tasks API.

Why do I not have script entries?

This may indicate that the long animation frame was not due to JavaScipt, but instead due to large render work.

This can also happen when the long animation frame is due to JavaScript but where the script attribution cannot be provided for various privacy reasons as noted previously (primarily that JavaScript not owned by the page).

Why do I have script entries but no, or limited, source information?

This can happen for a number of reasons, including there not being a good source to point to.

Script information will also be limited for no-cors cross-origin scripts, though this can be resolved by fetching those scripts using CORS by adding crossOrigin = "anonymous" to the <script> call.

For example, the default Google Tag Manager script to add to the page:

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->

Can be enhanced to add j.crossOrigin = "anonymous" to allow full attribution details to be provided for GTM

Will this replace the Long Tasks API?

While we believe the Long Animation Frames API is a better, more complete API for measuring long tasks, at this time, there are no plans to deprecate the Long Tasks API.

Feedback wanted

Feedback can be provided at the GitHub Issues list, or bugs in Chrome's implementation of the API can be filed in Chrome's issue tracker.

Conclusion

The Long Animation Frames API is an exciting new API with many potential advantages over the previous Long Tasks API.

It is proving to be a key tool for addressing responsiveness issues as measured by INP. INP is a challenging metric to optimize and this API is one way the Chrome team is seeking to make identifying and addressing issues easier for developers.

The scope of the Long Animation Frames API extends beyond just INP though, and it can help identify other causes of slow updates which can affect the overall smoothness of a website's user experience.

Acknowledgements

Thumbnail image by Henry Be on Unsplash.