Animation performance metrics beyond average FPS
Report callback intervals, long rendering updates, main-thread tasks and interaction latency as different measurements. Attach each result to its collection method, browser version, device class, display refresh rate and scenario; an average or an isolated count cannot supply that context for you.
How we measured it
Under the Tested conditions, the review wrapper preserves the P08 component and appends a separate observer. It resets and starts the workload programmatically, records callback timestamps and collects supported long-animation-frame entries. The sample includes the completion tail. Observer cadence is neither a count of progress assignments nor a display-presentation measurement.
The every-callback raw suite, 30-policy raw suite and analysis preserve exact values. Medians use the middle sorted value; p95 uses nearest rank, ceil(0.95 × n). Summary ranges below span per-run percentiles, not one pooled percentile.
| Shared P08 series (8.3 s windows) | Intervals by run | Median by run, ms | p95 by run, ms | LoAF counts |
|---|---|---|---|---|
| Every callback | 495, 497, 497, 497, 497 | 16.7 in all five | 17.3, 17.4, 17.5, 17.5, 17.4 | 0 in all five |
| 30-update policy | 497 in all five | 16.7 in all five | 17.5 in all five | 0 in all five |
The supported, active LoAF observer received no qualifying entries; this does not prove no presentation was missed. An earlier interrupted 30-policy suite is excluded in full, and the slight stop-timer boundary shortfall remains recorded. No separate P30 trial or rendering trace is implied.
Name the observation before the number
A callback interval is the difference between successive requestAnimationFrame timestamps. MDN describes the callback as running before a repaint and notes that calls commonly pause in hidden contexts.1 The interval therefore describes callback cadence in the recorded situation. It is not a counter of frames delivered to the display.
Retain the raw sequence: a summary can conceal pauses or a small group of larger intervals.
A frame’s work is not limited to your callback. Long Animation Frames entries describe rendering updates delayed beyond 50 milliseconds and can expose rendering and script attribution fields.2 Long Task entries instead describe an uninterrupted main-thread task of at least 50 milliseconds.3 The two APIs select different units of work. Do not substitute one count for the other.
Metric reference
| Observation | Unit | Collection basis | What it answers | Important boundary |
|---|---|---|---|---|
| Callback interval | ms | Successive animation callback timestamps1 | How far apart were these callbacks? | Not a displayed-frame counter. |
| Long animation frame | count; ms | long-animation-frame entries2 | Which rendering updates exceeded 50ms? | Not every delayed or dropped presentation. |
| Long task | count; ms | longtask entries3 | Which main-thread tasks lasted at least 50ms? | Not the complete rendering cycle. |
| Layout or paint event | count; ms | Named events in a selected trace range4 | How much recorded work occurred in that range? | Counts are not DOM-node counts or display frames. |
| Forced style/layout | ms | Script attribution where available5 | How much forced style/layout time was attributed? | Not all layout work or a universal attribution view. |
| Interaction to Next Paint | ms | Defined page-visit interaction latency calculation6 | How responsive was the page to eligible interactions? | Not continuous-animation cadence or hover/scroll timing. |
Read layout and paint in the selected trace
A layout count needs an event definition and a range. A paint count needs the same. The Chrome Performance panel exposes activities in trace order and filters by activity name and category.4 Record which events you counted and whether the range includes setup, the interaction or cleanup.
Keep duration alongside count. Ten small events and one large event have different costs; a trace entry is not a list of rendered nodes.
Forced synchronous layout can occur when code changes a geometric state and then reads information that requires the browser to resolve that change. Repeating that write/read cycle is layout thrashing.7 Inspect the relevant call path rather than assuming every layout entry came from an avoidable script pattern. Where available, forcedStyleAndLayoutDuration supplies attributed time, not a replacement for the complete trace.5
Keep interaction latency separate
INP uses click, tap and keyboard interaction latency across a page visit, with defined handling of outliers. It includes the period from the interaction’s start until the browser can next paint; it is not merely event-handler execution time.6 A short handler can coexist with waiting before it runs or work before the next paint.
A callback interval is not INP, and a controlled interaction sequence is not a result for all real visits.
Check availability and retain unknowns
PerformanceObserver.supportedEntryTypes reports which entry types the current user agent supports.8 Record that availability before collecting. An unsupported type means the corresponding result is unavailable; it does not mean that no long tasks or long frames occurred.
Keep null distinct from zero. Zero needs a known collector and window; null records an unavailable observation.
The reported display mode came from OS metadata, not callback gaps. It was not a continuous or physical refresh measurement.
The measured workload
This is the exact P08 component, reused here to make the workload inspectable. The observer lives outside it. Original code, MIT licensed.
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A timestamp-based progress control</title>
<style>
body { font: 1rem/1.5 system-ui; margin: 1rem; }
progress { display: block; width: 320px; max-width: 100%; height: 2rem; margin: 1rem 0; }
button, select { padding: .5rem; margin: .25rem; }
:focus-visible { outline: 3px solid #b24d00; outline-offset: 3px; }
</style>
</head>
<body>
<label for="cadence">Update policy</label>
<select id="cadence">
<option value="0">Every callback</option>
<option value="30">30 updates per second target</option>
</select>
<button id="start">Start or resume</button>
<button id="pause">Pause</button>
<button id="reset">Reset</button>
<progress id="progress" max="8000" value="0" aria-label="Eight-second progress"></progress>
<p id="state" role="status">Ready</p>
<script>
const progress = document.querySelector("#progress");
const state = document.querySelector("#state");
const cadence = document.querySelector("#cadence");
let elapsed = 0;
let last = null;
let lastPaint = 0;
let request = null;
let running = false;
function tick(time) {
if (!running) return;
if (last !== null) elapsed = Math.min(8000, elapsed + time - last);
last = time;
const interval = cadence.value === "30" ? 1000 / 30 : 0;
if (!interval || elapsed - lastPaint >= interval || elapsed === 8000) {
progress.value = elapsed;
lastPaint = interval ? elapsed - elapsed % interval : elapsed;
}
if (elapsed < 8000) request = requestAnimationFrame(tick);
else {
running = false;
request = null;
state.textContent = "Complete";
}
}
function pause(message) {
running = false;
cancelAnimationFrame(request);
request = null;
last = null;
state.textContent = message;
}
document.querySelector("#start").onclick = () => {
if (running || elapsed === 8000) return;
running = true;
last = null;
state.textContent = "Running";
request = requestAnimationFrame(tick);
};
document.querySelector("#pause").onclick = () => pause("Paused");
document.querySelector("#reset").onclick = () => {
pause("Ready");
elapsed = 0;
lastPaint = 0;
progress.value = 0;
};
document.addEventListener("visibilitychange", () => {
if (document.hidden && running) pause("Paused while hidden; resume when ready");
});
</script>
</body>
</html>
The existing record builder only formats supplied values. It does not collect these measurements or compute percentiles; its earlier 12 and 120 inputs remain interface-test examples, not observed layout or refresh values.
Not covered on this page
- A browser-wide collector or a hardware benchmark suite.
- GPU timing, video presentation or every rendering pipeline stage.
- A universal frame budget or a ranking of animation techniques.
- Field-data deployment, analytics consent or accessibility certification.
Sources
-
MDN, Window.requestAnimationFrame(). Living documentation, opened 24 October 2026. ↩ ↩2
-
MDN, PerformanceLongAnimationFrameTiming. Living documentation, opened 24 October 2026. ↩ ↩2
-
MDN, PerformanceLongTaskTiming. Living documentation, opened 24 October 2026. ↩ ↩2
-
Chrome for Developers, Performance features reference. Updated 3 April 2025; opened 24 October 2026. ↩ ↩2
-
MDN, forcedStyleAndLayoutDuration. Updated 19 April 2024; opened 24 October 2026. ↩ ↩2
-
web.dev, Interaction to Next Paint. Updated 2 September 2025; opened 24 October 2026. ↩ ↩2
-
web.dev, Avoid large, complex layouts and layout thrashing. Updated 7 May 2025; opened 24 October 2026. ↩
-
MDN, PerformanceObserver.supportedEntryTypes. Updated 12 October 2024; opened 24 October 2026. ↩
Code on this page is original and MIT licensed.
How we test: methods. Found a mistake? Corrections.