setTimeout vs requestAnimationFrame
Asked Answered
P

1

0

I made an example of 'setTimeout vs requestAnimationFrame' to find out how different they are.

As you can see, the orange box arrives to the destination first. The green box jump some times and slower.

I understand why the green box jump some times. Because the task(calling move function) would not be inserted in macroTaskQueue before repaint some times(this is called jank or frame skip).

This is why I prefer requestAnimationFrame than setTimeout when animate element. Because the move() of requestAnimationFrame(move) is guaranteed to be called right before repaint.

Now, what I'm wondering is that why the green box is slower than orange box

Maybe does it mean that the move() is not called at each 1000/60 ms?

Protamine answered 18/3, 2022 at 6:2 Comment(0)
J
3

setTimeout is always late.

The way it works is

  • Register a timestamp when to execute our task.
  • At each Event loop's iteration, check if now is after that timestamp.
  • Execute the task.

By this very design, setTimeout() is forced to take at least the amount of time defined by delay. It can (and will often) be more, for instance if the event loop is busy doing something else (like handling user gestures, calling the Garbage Collector, etc.).

Now since you are requesting a new timeout only from when the previous callback got called, your setTimeout() loop suffers from time-drift. Every iteration it will accumulate this drift and will never be able to recover from it, getting away from the wall-clock time.

requestAnimationFrame (rAF) on the other hand doesn't suffer from such a drift. Indeed, the monitor's V-Sync signal is what tells when the event loop must enter the "update the rendering" steps. This signal is not bound to the CPU activity and will work as a stable clock. If at one frame rAF callbacks were late by a few ms, the next frame will just have less time in between, but the flag will be set at regular intervals with no drift.

You can verify this by scheduling all your timers ahead of time, your setTimeout box won't suffer from this drift anymore:

const startBtn = document.querySelector('#a');
const jankBtn = document.querySelector('#b');
const settimeoutBox = document.querySelector('.settimeout-box');
const requestAnimationFrameBox = document.querySelector('.request-animation-frame-box');
settimeoutBox._left = requestAnimationFrameBox._left = 0;
let i = 0;

startBtn.addEventListener('click', () => {
  startBtn.classList.add('loading');
  startBtn.classList.add('disabled');
  scheduleAllTimeouts(settimeoutBox);
  moveWithRequestAnimationFrame(requestAnimationFrameBox);
});

function reset() {
  setTimeout(() => {
    startBtn.classList.remove('loading');
    startBtn.classList.remove('disabled');
    i = 0;
    settimeoutBox.style.left = '0px';
    requestAnimationFrameBox.style.left = '0px';
    settimeoutBox._left = requestAnimationFrameBox._left = 0;
  }, 300);
}

function move(el) {
  el._left += 2;
  el.style.left = el._left + 'px';
  if (el._left > 1000) {
    return false;
  }
  return true;
}

function scheduleAllTimeouts(el) {
  for (let i = 0; i < 500; i++) {
    setTimeout(() => move(el), i * 1000 / 60);
  }
}

function moveWithRequestAnimationFrame(el) {
  if (move(el)) {
    requestAnimationFrame(() => {
      moveWithRequestAnimationFrame(el);
    });
  } else reset();
}
.grid {
  margin: 30px !important;
  padding: 30px;
}

.box {
  width: 200px;
  height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  color: white;
  font-size: 18px;
}

.settimeout-box {
  background-color: green;
}

.request-animation-frame-box {
  background-color: orange;
}
<div class="ui grid container">
  <div class="row">
    <button class="ui button huge blue" id="a">Start!</button>
  </div>
  <div class="row">
    <div class="box settimeout-box">
      <span>setTimeout</span>
    </div>
  </div>
  <div class="row">
    <div class="box request-animation-frame-box">
      <span>requestAnimationFrame</span>
    </div>
  </div>
</div>

Note that Firefox and Chrome actually do trigger the painting frame right after the first call to rAF in a non-animated document, so rAF may be one frame earlier than setTimeout in this demo.


requestAnimationFrame's frequency is relative to the monitor's refresh-rate.

Above example assumes that you run it on a 60Hz monitor. Monitors with higher or lower refresh rate will enter this "update the rendering" step at different frequencies.


Also beware, delay in setTimeout(fn, delay) is a long, this means the value you pass will be floored to integer.

An a last note, Chrome does self correct this time drift in its setInteval() implementation, Firefox and the specs still don't, but it's under (not so active) discussion.

Jointly answered 18/3, 2022 at 7:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.