Search code examples
javascripttimerasync-awaitsettimeoutrequestanimationframe

Any advantages of using requestAnimationFrame for non-animation purposes?


I made a "sleep" timer function that recursively calls requestAnimationFrame and returns a promise after the time runs out. Is there any performance advantage in me using requestAnimationFrame for this or should I have just used setTimeout? I know requestAnimationFrame has better performance for very short/quick wait times (aka animation frames) but for wait times near or more than a second does it make a difference?

var sleep = (time) =>{
  var timer = (t, s, d, r)=>{
    if(t - s > d){
      r("done")
    } else {
      requestAnimationFrame((newT)=>{ timer(newT, s, d, r)})
    }
  }
  return new Promise((r)=>{
    requestAnimationFrame((t)=>{timer(t, t, time, r)})
  })
}

(async ()=>{
  var message = document.getElementById("message")
  while(message){
    message.innerText = "Get"
    await sleep(1000)
    message.innerText = message.innerText + " Ready"
    await sleep(1000)
    message.innerText = message.innerText + " To"
    await sleep(1000)
    message.innerText = message.innerText + " Wait"
    await sleep(1000)
  }
})()
<p style="text-align:center; font-size:20px;" id="message"></p>


Solution

  • requestAnimationFrame is not just a timing method, as its name says, it also does request an animation frame.

    In a browser, the event loop processing model does first execute normal tasks, and then, when the browser thinks it should update the rendering, it calls a special part of this processing model which will well, update the rendering.

    In most event loop iterations, the conditions required to enter this optional part of the processing model are not met, and thus, no rendering is done, which is great because rendering is costly.

    Even though the specs leave to implementers the choice of what heuristics they'll use to decide when they should update the rendering, most will use the monitor's refresh-rate as base frequency.
    In addition to this monitor's refresh event, they only update the rendering of the Documents that they marked as needing an update.
    Most of the time, a web page doesn't need to be re-rendered, and only when an animation is running, or when the user did interact with it, they'll mark the document as being in need of a re-render.

    requestAnimationFrame is a tool for us web-devs to hook in this update the rendering sub-process, allowing us to draw things only when the rendering will happen, but it also has the side effect of marking the web page as animated, and thus forces browser to execute the full update the rendering steps even though it might not have been needed.

    For instance, using Chrome's Performance dev tools, you can see in below snippet that a simple empty rAF loop will cause the Composite Layer operation to get triggered every screen-refresh, while it would happen only when the mouse moves otherwise, and just because mouse-move internally calls rAF).

    const input = document.querySelector("input");
    input.oninput = (evt) => {
      rAFLoop();
    };
    function rAFLoop() {
      if( input.checked ) {
        requestAnimationFrame( rAFLoop );
      }
    }
    <label><input type="checkbox" id="inp">activate rAF loop</label>

    So while requestAnimationFrame is a great tool for visual animations, it also comes with responsibilities, and in your case, you are misusing it.

    Using a simple setTimeout is the way to go, not only it won't have anything to do with the rendering process, but it can even let the browser go idle if nothing else happens, letting your computer do other tasks, or saving trees.

    const sleep = (ms) =>
      new Promise( (res) =>
        setTimeout( () => res(), ms )
      );
    (async ()=>{
      var message = document.getElementById("message")
      while(message){
        message.innerText = "Get"
        await sleep(1000)
        message.innerText = message.innerText + " Ready"
        await sleep(1000)
        message.innerText = message.innerText + " To"
        await sleep(1000)
        message.innerText = message.innerText + " Wait"
        await sleep(1000)
      }
    })()
    <p style="text-align:center; font-size:20px;" id="message"></p>