Reputation: 54099
Why should I use requestAnimationFrame rather than setTimeout or setInterval?
This self-answered question is a documentation example.
Upvotes: 135
Views: 87088
Reputation: 54099
This post is very old and there have been many changes since I first posted the question.
requestAnimationFrame is still the best option rather than setTimeout and setInterval.
The main reason is that requestAnimationFrame is synced to the display refresh rate.
Specifically the vertical blank, vertical sync, or vSync (analogous to old CRT displays). This is a time in the display process that pixels on the display are not being updated and changes can be made to VRAM while not affecting the displayed pixels.
requestAnimationFrame will call your render function well before the next vSync, but after the previous vSync. When your render function returns requestAnimationFrame will wait till after the next vSync before calling your render function again (Be sure to return in time).
The following image shows a frame (example rate 60 frames per second). The requestAnimationFrame callback will be called before the next vSync. It will be called as early as possible. You should ensure you return from it before the next vSync
setTimeout and setInterval are not aware of the frame rate or when the vSynce starts and ends.
With well written listener you can match the animation quality you get from CSS animations across devices.
The DOM uses the vSync time to update the GPU RAM with all the changes made during the previous frame (including any changes made by any timer and requestAnimationFrame listeners)
requestAnimationFrame also passes a high resolution time to the callback. This time is the same time you get from the performance API. eg performance.now()
You can use the time argument to get the frame rate and determine a delta time for your animations
Example getting delta time using requestAnimationFrame
requestAnimationFrame(mainRenderLoop);
var prevFrameTime, deltaTime;
function mainRenderLoop(time) {
if (prevFrameTime === undefined) {
deltaTime = 0;
} else {
deltaTime = time - prevFrameTime;
}
prevFrameTime = time;
//
// rendering code here
//
requestAnimationFrame(mainRenderLoop);
}
Because the callback is synced to the display refresh rate the frame rate will vary from device to device. You should use the time argument to determine the rate and adjust your animation as needed.
Like other timers requestAnimationFrame will stop calling the callback when the page is hidden. Eg client switches Tabs, another window hides the browser window.
It is possible for the client to setup the GPU drivers and browser to ignore the display refresh rate. You can not detect this directly.
requestAnimationFrame assumes the time the callback returns is the frame that is being animated. It will not call next frame until after the next vSync
Taking too long to render a frame will cause requestAnimationFrame
to skip frames resulting in Jank.
requestAnimationFrame
produces higher quality animation completely eliminating flicker and shear that can happen when using setTimeout
or setInterval
, and reduce or completely remove frame skips.
is when a new canvas buffer is presented to the display buffer midway through the display scan resulting in a shear line caused by the mismatched animation positions.
is caused when the canvas buffer is presented to the display buffer before the canvas has been fully rendered.
is caused when the time between rendering frames is not in precise sync with the display hardware. Every so many frames a frame will be skipped producing inconsistent animation. (There are method to reduce this but personally I think these methods produce worse overall results) As most devices use 60 frames per second (or multiple of) resulting in a new frame every 16.666...ms and the timers setTimeout
and setInterval
use integers values they can never perfectly match the framerate (rounding up to 17ms if you have interval = 1000/60
)
Update The answer to the question requestAnimationFrame loop not correct fps shows how setTimeout's frame time is inconsistent and compares it to requestAnimationFrame.
The demo shows a simple animation (stripes moving across the screen) clicking the mouse button will switch between the rendering update methods used.
There are several update methods used. It will depend on the hardware setup you are running as to what the exact appearance of the animation artifacts will be. You will be looking for little twitches in the movement of the stripes
Note. You may have display sync turned off, or hardware acceleration off which will affect the quality of all the timing methods. Low end devices may also have trouble with the animation
Timer Uses setTimeout to animate. Time is 1000/60
RAF Best Quality, Uses requestAnimationFrame to animate
Dual Timers, Uses two timers, one called every 1000/60 clears and another to render.
UPDATE OCT 2019 There have been some changes in how timers present content. To show that setInterval
does not correctly sync with the display refresh I have changed the Dual timers example to show that using more than one setInterval
can still cause serious flicker The extent of the flickering this will produce depends on hardware set up.
RAF with timed animation, Uses requestAnimationFrame but animates using frame elapsed time. This technique is very common in animations. I believe it is flawed but I leave that up to the viewer
Timer with timed animation. As "RAF with timed animation" and is used in this case to overcome frame skip seen in "Timer" method. Again I think it sucks, but the gaming community swears it is the best method to use when you don't have access to display refresh
/** SimpleFullCanvasMouse.js begin **/
var backBuff;
var bctx;
const STRIPE_WIDTH = 250;
var textWidth;
const helpText = "Click mouse to change render update method.";
var onResize = function(){
if(backBuff === undefined){
backBuff = document.createElement("canvas") ;
bctx = backBuff.getContext("2d");
}
backBuff.width = canvas.width;
backBuff.height = canvas.height;
bctx.fillStyle = "White"
bctx.fillRect(0,0,w,h);
bctx.fillStyle = "Black";
for(var i = 0; i < w; i += STRIPE_WIDTH){
bctx.fillRect(i,0,STRIPE_WIDTH/2,h) ;
}
ctx.font = "20px arial";
ctx.textAlign = "center";
ctx.font = "20px arial";
textWidth = ctx.measureText(helpText).width;
};
var tick = 0;
var displayMethod = 0;
var methods = "Timer,RAF Best Quality,Dual Timers,RAF with timed animation,Timer with timed animation".split(",");
var dualTimersActive = false;
var hdl1, hdl2
function display(timeAdvance){ // put code in here
tick += timeAdvance;
tick %= w;
ctx.drawImage(backBuff,tick-w,0);
ctx.drawImage(backBuff,tick,0);
if(textWidth !== undefined){
ctx.fillStyle = "rgba(255,255,255,0.7)";
ctx.fillRect(w /2 - textWidth/2, 0,textWidth,40);
ctx.fillStyle = "black";
ctx.fillText(helpText,w/2, 14);
ctx.fillText("Display method : " + methods[displayMethod],w/2, 34);
}
if(mouse.buttonRaw&1){
displayMethod += 1;
displayMethod %= methods.length;
mouse.buttonRaw = 0;
lastTime = null;
tick = 0;
if(dualTimersActive) {
dualTimersActive = false;
clearInterval(hdl1);
clearInterval(hdl2);
updateMethods[displayMethod]()
}
}
}
//==================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
const U = undefined;const RESIZE_DEBOUNCE_TIME = 100;
var w,h,cw,ch,canvas,ctx,mouse,createCanvas,resizeCanvas,setGlobals,globalTime=0,resizeCount = 0;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () { var c,cs; cs = (c = document.createElement("canvas")).style; cs.position = "absolute"; cs.top = cs.left = "0px"; cs.zIndex = 1000; document.body.appendChild(c); return c;}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); } canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); } if (typeof onResize === "function"){ resizeCount += 1; setTimeout(debounceResize,RESIZE_DEBOUNCE_TIME);}
}
function debounceResize(){ resizeCount -= 1; if(resizeCount <= 0){ onResize();}}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; mouse.updateBounds(); }
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0, over : false, bm : [1, 2, 4, 6, 5, 3],
active : false,bounds : null, crashRecover : null, mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.clientX - m.bounds.left; m.y = e.clientY - m.bounds.top;
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
if((m.buttonRaw & 2) && m.crashRecover !== null){ if(typeof m.crashRecover === "function"){ setTimeout(m.crashRecover,0);}}
e.preventDefault();
}
m.updateBounds = function(){
if(m.active){
m.bounds = m.element.getBoundingClientRect();
}
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
m.active = true;
m.updateBounds();
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
m.active = false;
}
}
return mouse;
})();
resizeCanvas();
mouse.start(canvas,true);
onResize()
var lastTime = null;
window.addEventListener("resize",resizeCanvas);
function clearCTX(){
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h); // though not needed this is here to be fair across methods and demonstrat flicker
}
function dualUpdate(){
if(!dualTimersActive) {
dualTimersActive = true;
hdl1 = setInterval( clearCTX, 1000/60);
hdl2 = setInterval(() => display(10), 1000/60);
}
}
function timerUpdate(){
timer = performance.now();
if(!lastTime){
lastTime = timer;
}
var time = (timer-lastTime) / (1000/60);
lastTime = timer;
setTimeout(updateMethods[displayMethod],1000/60);
clearCTX();
display(10*time);
}
function updateRAF(){
clearCTX();
requestAnimationFrame(updateMethods[displayMethod]);
display(10);
}
function updateRAFTimer(timer){ // Main update loop
clearCTX();
requestAnimationFrame(updateMethods[displayMethod]);
if(!timer){
timer = 0;
}
if(!lastTime){
lastTime = timer;
}
var time = (timer-lastTime) / (1000/60);
display(10 * time);
lastTime = timer;
}
displayMethod = 1;
var updateMethods = [timerUpdate,updateRAF,dualUpdate,updateRAFTimer,timerUpdate]
updateMethods[displayMethod]();
/** SimpleFullCanvasMouse.js end **/
Upvotes: 177
Reputation: 137084
Which one is better will depend on the use case. So I'll try to enumerate some differences and when one is more suited than the another.
We can assume that setTimeout
and setInterval
are basically equivalent: they schedule a callback to fire after a given delay, where setInterval
is just a looping setTimeout
. setInterval
does bring a little bit of magic in Chromium's implementation in that they will try to correct the drift caused by the delay in the setTimeout
execution. This is against the specs, but discussions are ongoing to make it the standard behavior for this method.
Now both methods will fight with other tasks queued by the different APIs before they can run. Even if it's not specced anywhere, timer tasks generally have a "user-visible" level of priority, so usually UI events will have higher priority, and network ones would have a lower one.
So the time their callback is ran can be relatively long after you asked it to fire, or it could be just on time, and you don't have much control on this behavior.
requestAnimationFrame
on the other hand will queue an animation callback that will be called in the special update the rendering task. The browser has full control on when it will execute this task and they'll generally try to make it match the active monitor's refresh rate, or ASAP when the document wasn't animated yet. Beware that this means that your callbacks may fire at different rate depending on your user's monitor.
This update the rendering task will usually have a priority corresponding to "user-blocking", meaning that even if other tasks have been queued before, it should win. And this even if the callbacks take almost all the time available between two rendering frames. Your callbacks will still have to fight with other callbacks that are part of the update the rendering task, like some pointer events, scroll, resize events, Web animations, IntersectionObserver or ResizeObserver callbacks, etc. but most tasks will get lower priority.
This allows the browser to ensure it can update the rendering smoothly at every monitor's refresh. In case a rendering frame takes too long to be presented, it will present the next one ASAP to the OS compositor, because the rendering is at least double buffered: the browser compositor is always one frame in advance, and then it passes its bitmap to the OS compositor which will then pass it to the monitor. In case the rendering is late by more than one frame, the browser may not run the next one ASAP and instead start its usual scheduling, which is known as "dropping" a frame.
requestAnimationFrame
is also subject to stronger throttling rules than timers. Indeed its main purpose is to do something visual, so when the document is not visible, it makes sense that the callbacks in there aren't ran. Timers also are throttled when the document is not visible but it takes generally about a minute before it happens, and the browser may still fire it at some interval, while animation callbacks are usually entirely discarded.
Another point is that requestAnimationFrame
will force the browser to queue an update the rendering task, even if nothing on the page did change and it would have avoided it otherwise. So having a running requestAnimationFrame
loop, even if it does nothing, may actually come at some (minor) cost.
To recap:
If one wants to animate something visual, as smooth as possible, requestAnimationFrame
is the clear winner1. With timers there is too much uncertainty whether you'll match the correct frame rate, not only is it really hard to determine the user's monitor's refresh rate, but you're not even sure your timers will hit the requested rate, and they may drift. If it goes too slow you'll miss frames and have janked animations, if it goes too fast you'll render useless frames, possibly eating the resources needed for a future frame.
If one wants to execute a callback that does not update something visual, or at a lower frame rate than the monitor's refresh rate, then you're probably better without using requestAnimationFrame
.
If one wants to fire a callback at an higher frequency than the monitor's refresh rate, be sure it's not for too long. Timers will have a 4ms cap after the 5th round, but you can circumvent this by using either scheduler.postTask()
or a MessageChannel
instead.
If one wants to throttle some fast firing events, then both can make sense. If the events are likely to cause a visual change, requestAnimationFrame
would probably be the winner. Though you may also be interested in the fact that ResizeObserver
's callbacks do fire after rAF and after the browser has done its relayout, so it may be a very good place for these instead if you don't need to read the layout from there.
If one wants to render an OffscreenCanvas
in a Web Worker, then use requestAnimationFrame
. Failing to do so you can't be sure the placeholder canvas would get updated.
If one wants to make a battery-friendly timer that would pause whenever the document is not visible anymore, then wrapping the call to your timer in an animation frame may make a nice hack.
If, on the contrary someone needs to run something when the document is not visible, neither are very good solutions. Workers might work, the Web Audio API's scheduler nodes would work, but are impractical.
1. Other APIs like Web Animations may make a good contender depending on the situation, but we only compare timers and rAF here.
Upvotes: 2
Reputation: 11
When we use setTimeout
or setInterval
it's redundant because setTimeout
or setInterval
perform a task almost 4 time in which only the last one is necessary so the first 3 is redundant and of no use just taking more CPU power but requestAnimationFrame takes just one step so we can roughly say requestAnimationFrame is almost 4 time better than setTimeout or setInterval
Upvotes: -2