requestAnimationFrame(cb)
schedules cb
to be called just before the next page rendering.
So when the callback is executed, the rendering wasn't done yet.
However, scheduling a 0
ms timeout from the rAF callback does allow to fire a function after the rendering happened.
To measure how long the rendering actually took, you indeed want to get the time both before and after the rendering occurred.
Note that there used to be a requestPostAnimationFrame
proposal, so that we could have a native hook to this exact place in the event-loop, but it seems it hasn't moved a lot in the last few years, so I wouldn't hold my breath until it's widely supported. But you can still use the polyfill I made for this other SO Q/A, which does use a faster task scheduling than setTimeout
, so that we're more confident to really be as close as possible from the rendering step.
Also, to ensure that your rAF callback is fired as late as possible, you'll want it to be scheduled from that postAnimationFrame callback (i.e the one from setTimeout
), so that there is as few callbacks firing in between as possible. Scheduling it from the rAF callback directly, you could have other raF loops stacked after your callback and you would also measure the JS execution of these functions instead of measuring only the rendering time of your page. Note that even doing so, you can still have rAF callbacks scheduled later, for instance if they are scheduled from a click event.
// requestPostAnimationFrame polyfill
// from https://stackoverflow.com/a/57549862/3702797
"function"!=typeof requestPostAnimationFrame&&(()=>{const a=new MessageChannel,b=[];let c=0,d=!1,e=!1,f=!1;a.port2.onmessage=()=>{d=!1;const a=b.slice();b.length=0,a.forEach(a=>{try{a(c)}catch(a){}})};const g=globalThis.requestAnimationFrame;globalThis.requestAnimationFrame=function(...a){e||(e=!0,g.call(globalThis,a=>f=a),globalThis.requestPostAnimationFrame(()=>{e=!1,f=!1})),g.apply(globalThis,a)},globalThis.requestPostAnimationFrame=function(e){if("function"!=typeof e)throw new TypeError("Argument 1 is not callable");b.push(e),d||(f?(c=f,a.port1.postMessage("")):requestAnimationFrame(b=>{c=b,a.port1.postMessage("")}),d=!0)}})();
// measure as close as possible the time it took for rendering the page
function measureRenderingTime(cb) {
let t1, t2;
const rAFCallback = () => {
requestPostAnimationFrame(rPAFCallback);
t1 = performance.now(); // before rendering
};
const rPAFCallback = () => {
t2 = performance.now(); // after rendering
cb(t2 - t1);
};
requestAnimationFrame(rAFCallback);
}
// do something with the measurements
// (here simple max + average over 50 occurrences)
const times = [];
const handleRenderingTime = (time) => {
times.push(time);
while (times.length > 50) {
times.shift();
}
document.querySelector("pre").textContent = `last: ${ time.toFixed(2) }
max: ${ Math.max(...times).toFixed(2) }
avg: ${ (times.reduce((t,v) => t+v, 0) / times.length).toFixed(2) }`;
// loop
measureRenderingTime(handleRenderingTime);
};
measureRenderingTime(handleRenderingTime); // begin the loop
// simulate an actual rAF loop, with sometimes long JS execution time
let longFrame = false;
document.querySelector("button").onclick = (evt) => longFrame = true;
const animLoop = () => {
if (longFrame) {
const t1 = performance.now();
// lock the browser for 300ms
// this is actually only JS, that shouldn't impact the rendering time by much
while (performance.now() - t1 < 300) {}
};
longFrame = false;
requestAnimationFrame(animLoop);
};
requestAnimationFrame(animLoop);
<pre id=log></pre>
<button>perform a long rAF callback</button>