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
Following an origin trial from Chrome 116 to Chrome 122, the LoAF API has shipped from Chrome 123.
Background: the Long Tasks API
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
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, as well as get insights into smoothness issues.
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 200 milliseconds 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 are measured from the start of tasks which require a render. Where the first task in a potential long animation frame does not require a render, the long animation frame is ended upon completion of the non-rendering task and a new potential long animation frame is started with the next task. Such non-rendering long animation frame are still included in the Long Animation Frames API when greater than 50 milliseconds (with a renderStart
time of 0) to allow measuring of potentially blocking work.
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.
Frame timestamps and durations
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 includesrequestAnimationFrame
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 total duration in milliseconds for which the animation frame would block processing of input or other high priority tasks.
An explanation of blockingDuration
A long animation frame may be made up of a number of tasks. The blockingDuration
is the sum of task durations longer than 50 milliseconds (including the final render duration within the longest task).
For example, if a long animation frame was made up of two tasks of 55 milliseconds and 65 milliseconds followed by a render of 20 milliseconds, then the duration
would be approximately 140 milliseconds with a blockingDuration
of (55 - 50) + (65 + 20 - 50) = 40 milliseconds. For 40 milliseconds during this 140 millisecond long animation frame, the frame was considered blocked from handling input.
Whether to look at duration
or blockingDuration
For the common 60 hertz display, a browser will try to schedule a frame at least every 16.66 milliseconds (to ensure smooth updates), or after a high priority task like input handling (to ensure responsive updates). However, if there is no input—nor other high-priority tasks—but there is a queue of other tasks, the browser will typically continue the current frame well past 16.66 milliseconds no matter how well broken up the tasks are within it. That is, the browser will always try to prioritise inputs, but may choose to tackle a queue of tasks over render updates. This is due to rendering being an expensive process so processing a combined rendering task for multiple tasks usually leads to an overall reduction of work.
Therefore, long animation frames with a low or zero blockingDuration
should still feel responsive to input. Reducing or eliminating blockingDuration
by breaking up long tasks is therefore key to improving responsiveness as measured by INP.
However, a lot of long animation frames, regardless of blockingDuration
indicates UI updates that are delayed and so can still affect smoothness and lead to a feeling of a laggy user interface for scrolling or animations, even if these are less of an issue for responsiveness as measured by INP. To understand issues in this area look at the duration
, but these can be more tricky to optimize for since you cannot solve this by breaking up work, but instead must reduce work.
Frame timings
The previously mentioned 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 |
Better script attribution
The long-animation-frame
entry type includes better attribution data of each script that contributed to a long animation frame (for scripts longer than 5 milliseconds).
Similar to the Long Tasks API, this will be provided in an array of attribution entries, each of which details:
- A
name
andEntryType
both will returnscript
. - 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 perresolve-promise
, but for the rejection.classic-script
: Script evaluation (for example,<script>
orimport()
)module-script
: Same asclassic-script
, but for module scripts.
- Separate timing data for that script:
startTime
: Time the entry function was invoked.duration
: The duration betweenstartTime
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
}
Link to the longest INP interaction
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:
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 is processed in the next frame:
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 only looking at the INP LoAF, you may want to consider all LoAFs across the page lifetime:
However, each LoAF entry contains considerable data, so you will likely want to restrict your analysis to only some LoAFs. Additionally, 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.
Some suggested patterns to reduce the amount of long animation frame data includes:
- Observe long animation frames with interactions
- Observe long animation frames with high blocking durations
- Observe long animation frames during critical UI updates to improve smoothness
- Observe the worst long animation frames
- Identify common patterns in long animation frames
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 resolve your common responsiveness issues, you may expand this by not limiting to just interactions or high blocking durations or by lowering thresholds.
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) with a high blockingDuration
.
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 with a blockingDuration
greater than 100 milliseconds where an interaction occurred during the frame. The 100 is chosen here because it is less than the 200 millisecond "good" INP threshold. You could choose a higher or lower value depending on your needs.
const REPORTING_THRESHOLD_MS = 100;
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.blockingDuration > 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 long animation frames with high blocking durations
As an improvement to looking at all long animation frames with interactions, you may want to look at all long animation frames with high blocking durations. These indicate potential INP problems if a user does interact during these long animation frames.
The following code logs all LoAF entries with a blocking duration greater than 100 milliseconds where an interaction occurred during the frame. The 100 is chosen here because it is less than the 200 millisecond "good" INP threshold to help identify potential problem frames, while keeping the amount of long animation frames reported to a minimum. You could choose a higher or lower value depending on your needs.
const REPORTING_THRESHOLD_MS = 100;
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.blockingDuration > 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 });
Observe long animation frames during critical UI updates to improve smoothness
As mentioned previously, looking at high blocking duration long animation frames can help to address input responsiveness. But for smoothness you should look at all long animation frames with a long duration
.
Since, this can get quite noisy you may want to restrict measurements of these to key points with a pattern like this:
const REPORTING_THRESHOLD_MS = 100;
const observer = new PerformanceObserver(list => {
if (measureImportantUIupdate) {
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 });
async function doUIUpdatesWithMeasurements() {
measureImportantUIupdate = true;
await doUIUpdates();
measureImportantUIupdate = false;
}
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 on, five, ten, 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 100 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. Using the DevTools Extensibility API these can even be shown in their own track:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
performance.measure('LoAF', {
start: entry.startTime,
end: entry.startTime + entry.duration,
detail: {
devtools: {
dataType: "track-entry",
track: "Long animation frames",
trackGroup: "Performance Timeline",
color: "tertiary-dark",
tooltipText: 'LoAF'
}
}
});
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
Longer term, long animation frames will likely be incorporated into DevTools itself, but the previous code snippet allows it to be surfaced there in the meantime.
The first entry in the previous figure also demonstrates where the browser has processed several tasks together in the same long animation frame rather than render between them. As mentioned previously this can happen when there are no high-priority input tasks, but there is a queue of tasks. The first long task has some render updates to complete (otherwise the current long animation frame would be reset after it, and a new one would start with the next task), but instead of actioning that render immediately, the browser has processed a number of additional tasks and only then actioned the long render task and ended the long animation frame. This demonstrates the usefulness of looking at long animation frames in DevTools, rather than just long tasks, to help identify delayed renders.
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:
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.