4
var y = 0
canvas.height *= 5
ctx.fillStyle = 'green'
function update () {
  requestAnimationFrame(update)
   ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.fillRect(0, y, 300, 300)
  y++
}
update()

For this simple JSBin (https://jsbin.com/yecowob/edit?html,js,output) where a square moves along the screen, this is how the Chrome dev tools timeline looks:

https://i.stack.imgur.com/QqrW5.jpg

As I understand, the vertical dotted grey line is the end of the current frame and the start of the next one. In the screenshot we have a 19.3 ms frame, where the browser barely does anything (a lot of idle time). Couldn't the browser avoid this if it just ran all the code right as the frame started?

If, however, I draw the square 500 times, on 6x CPU slowdown (https://jsbin.com/yecowob/4/edit?js,output), I get periods when the browser does exactly what I want (to start running the code as the frame starts), but it gets out of sync again:

https://i.stack.imgur.com/vjJrW.jpg

When it starts running on the dotted line, the fps is much smoother, but I can only get it to work when the browser has some heavy lifting to do.

So why doesn't requestAnimationFrame() trigger right at the start of the frame every time, and how can I make it do so?

Many thanks for any help you can give me on this.

Dan Birsan
  • 41
  • 3

2 Answers2

3

Because that's what requestAnimationFrame does: it schedules a callback to fire at the next "painting frame", just before the actual painting to screen.

Here "frame" refers to an Event Loop iteration, not a visual one, and hereafter I'll keep using "Event Loop iteration" to make the distinction.

So if we take a look at the structure of an Event Loop iteration as described by the HTML specs, we can see that "run the animation frame callbacks" algorithm is called from inside the "update the rendering" algorithm.
This algorithm is responsible at step 2 of determining if the current Event Loop iteration is a painting one or not, by checking the "rendering opportunities" of each active Documents. If it isn't, then all the inner steps below are discarded, including our "run the animation frame callbacks".
This means that our requestAnimationFrame scheduled callbacks will only get executed in a very special Event Loop Iteration: the next one with a rendering opportunity.

Specs don't describe precisely at which frequency this "painting frames" should occur, but basically most current vendors try to maintain 60Hz, while Chrome will make it cap to the active display's refresh rate. It is expected that Chrome behavior spreads to other vendors.


So what you describe is normal. If you want a simplified version of this, you can think of requestAnimationFrame( fn ) as setTimeout( fn, time_needed_until_the_next_painting_frame ) (with the here minor difference that timedout callbacks are executed at the beginning of the Event Loop iteration while animation frame callbacks are executed at the end).

Why has it been designed this way?

Well because most of the time we want to have the freshest information painted on the screen. So having these callbacks to fire right before the painting ensures that everything that should be painted is at its most recent position.

But this also means that indeed, we should not have too heavy operations occurring in there, at the risk of loosing a painting opportunity.


Now, I have to note that there is an ongoing proposal to include a requestPostAnimationFrame, which would schedule callbacks to fire at the next "painting frame", just after the actual painting to screen.
With this method, you'd have the behavior you expected.
Unfortunately, that's still just a proposal, has not been included to the specs, and it's uncertain if it will ever be.

Though it is already implemented in Chrome, behind the "Experimental Web Platform features" flag, the best we can do to approach its behavior in normal browsers is to schedule a callback at the very beginning of the next Event Loop iteration.
Here is an example implementation I made for an other Q/A:

if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

requestAnimationFrame( animationFrameCallback );
requestPostAnimationFrame( postAnimationFrameCallback );

// monkey-patches requestPostAnimationFrame
//!\ Can not be called from inside a requestAnimationFrame callback
function monkeyPatchRequestPostAnimationFrame() {
  console.warn('using a MessageEvent workaround');
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    called = false;
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
  }
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
    if (!called) {
      requestAnimationFrame((time) => {
        timestamp = time;
        channel.port1.postMessage('');
      });
      called = true;
    }
  };
}


// void loops, look at your dev-tools' timeline to see where each fires
function animationFrameCallback() {
  requestAnimationFrame( animationFrameCallback );
}
function postAnimationFrameCallback() {
  requestPostAnimationFrame( postAnimationFrameCallback )
}
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Hi @Kaiido. Thank you very much for this detailed explanation. While it did not solve my problem, it help me better understand it, although there is still one aspect I can't wrap my mind around: With requestPostAnimationFrame() I can get my code to run right after drawing the frame, but the Rendering and Painting done by the Compositor still get called at the end of the frame. In my second screenshot (https://imgur.com/Y04XCrz), Rendering and Painting get called right after my code is run (this is what I want), but I don't know what causes this and if it's possible to influence it in any way. – Dan Birsan Sep 28 '19 at 09:53
  • What I noticed is that here: https://imgur.com/a/Kwi55aY, in Compositor, Frame Start (the 2nd one) triggers just a bit BEFORE Draw Frame. This is what happens for all the frames that do what I want; Frame Start triggers just before what I suppose is the Draw Frame (vertical dotted line) of the current frame. – Dan Birsan Sep 28 '19 at 10:15
  • That's because it's how the event loop is designed. If you look at the links in my answer you'll see how all the compositing operations are also to be made on these "painting frames" and no, for this there is nothing you can do to change when they happen. – Kaiido Sep 28 '19 at 11:56
0

I test your second code snip in the newest chrome and found the result was same as the first one:

enter image description here

So just refer @kaiido's answer. It is the right answer i think.

tomwang1013
  • 1,349
  • 2
  • 13
  • 27