Long Animation Frames
This snippet requires specific features that are either available in Chrome 115+ for origins that are part of the LoAF Origin Trial (opens in a new tab) or require that you enable the #enable-experimental-web-platform-features flag in chrome://flags
(please note that you will need to restart your browser).
To determine when long animation frames (LoAF) happen, you can use PerformanceObserver (opens in a new tab) and register to observe entries of type long-animation-frame
. This snippet from @noamr (opens in a new tab) provides additional metrics computed from the LoAF raw data.
Snippet
(function init() {
function processAndFilterLoAFs(entries) {
function floorObject(o) {
return Object.fromEntries(
Array.from(Object.entries(o)).map(([key, value]) => [
key,
typeof value === "number" ? Math.floor(value) : value,
]),
);
}
function processEntry(entry) {
const startTime = entry.startTime;
const endTime = entry.startTime + entry.duration;
const delay = entry.desiredRenderStart
? Math.max(0, entry.startTime - entry.desiredRenderStart)
: 0;
const deferredDuration = Math.max(
0,
entry.desiredRenderStart - entry.startTime,
);
const renderDuration = entry.styleAndLayoutStart - entry.renderStart;
const workDuration = entry.renderStart
? entry.renderStart - entry.startTime
: entry.duration;
const totalForcedStyleAndLayoutDuration = entry.scripts.reduce(
(sum, script) => sum + script.forcedStyleAndLayoutDuration,
0,
);
const styleAndLayoutDuration = entry.styleAndLayoutStart
? endTime - entry.styleAndLayoutStart
: 0;
const scripts = entry.scripts.map((script) => {
const delay = script.startTime - script.desiredExecutionStart;
const scriptEnd = script.startTime + script.duration;
const compileDuration = script.executionStart - script.startTime;
const execDuration = scriptEnd - script.executionStart;
return floorObject({
delay,
compileDuration,
execDuration,
...script.toJSON(),
});
});
return floorObject({
startTime,
delay,
deferredDuration,
renderDuration,
workDuration,
styleAndLayoutDuration,
totalForcedStyleAndLayoutDuration,
...entry.toJSON(),
scripts,
});
}
return entries.map(processEntry);
}
function analyze() {
return loafs
.map((loaf) => ({
blockingDuration: loaf.blockingDuration,
loaf,
scripts: loaf.scripts,
events: events.filter((e) => overlap(e, loaf)),
}))
.filter((l) => l.blockingDuration && l.events.length);
}
let loafs = [];
let events = [];
function processLoAFs(entries) {
loafs = [...loafs, ...processAndFilterLoAFs(entries.getEntries())];
console.log(analyze());
}
function processEvents(entries) {
events = [...events, ...entries.getEntries()];
console.log(analyze());
}
new PerformanceObserver(processLoAFs).observe({
type: "long-animation-frame",
buffered: true,
});
new PerformanceObserver(processEvents).observe({
type: "event",
buffered: true,
});
function overlap(e1, e2) {
return (
e1.startTime < e2.startTime + e2.duration &&
e2.startTime < e1.startTime + e1.duration
);
}
window.whynp = analyze;
})();