Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When will requestAnimationFrame be executed?

Browser reads and runs a JavaScript file, the synchronous tasks written in the file immediately become in-mid-execution task, setTimeout callbacks become macrotasks, and promise callbacks become microtasks. Everything is good.

I thought I mastered the JavaScript Event Loop until I met requestAnimationFrame.

@T.J. Crowder provided me with the following code snippet.

const messages = [];
setTimeout(() => {
  // Schedule a microtask
  Promise.resolve().then(() => {
    log("microtask");
  });
  
  // Schedule animation frame callback
  requestAnimationFrame(() => {
    log("requestAnimationFrame");
  });
  
  // Schedule a macrotask
  setTimeout(() => {
    log("macrotask");
  }, 0);
  
  // Schedule a callback to dump the messages
  setTimeout(() => {
    messages.forEach(msg => {
      console.log(msg);
    });
  }, 200);

  // Busy-wait for a 10th of a second; the browser will be eager to repaint when this task completes
  const stop = Date.now() + 100;
  while (Date.now() < stop) {
  }
  
}, 100);

function log(msg) {
  messages.push(Date.now() + ": " + msg);
}
  • Chrome: microtask, requestAnimationFrame, macrotask
  • Firefox: microtask, macrotask, requestAnimationFrame

The spec doesn't say whether that can happen between the completion of a macrotask and the processing of its scheduled microtasks, or only between macrotasks. So presumably that can vary browser to browser.

But in both Chrome and Firefox, microtasks are always executed before requestAnimationFrame callbacks. My questions below are based on this observation.


**Q1: **

Even though the browser has no repaint work, will the requestAnimationFrame's callback be executed at the refresh rate (default 60 per second)?


**Q2: **

Below is from https://developers.google.com/web/fundamentals/performance/rendering/debounce-your-input-handlers

The only way to guarantee that your JavaScript will run at the start of a frame is to use requestAnimationFrame.

Too heavy in-mid-execution task will lag the browser, cause the frame interval exceed 16.66ms, block frames from completing.

Does the word 'guarantee' mean that the microtasks will be in-mid-execution immediately the current JS stack becomes empty, hence block the current frame from completing (if the microtask is also too heavy)?

like image 375
Shuumatsu Avatar asked Mar 27 '17 15:03

Shuumatsu


People also ask

How often is requestAnimationFrame called?

requestAnimationFrame() is 1 shot. You should call this method whenever you're ready to update your animation onscreen. This will request that your animation function be called before the browser performs the next repaint.

When can I use requestAnimationFrame?

The requestAnimationFrame() method tells the browser to run a callback function right before the next repaint happens. It's particularly useful when using JavaScript for animations and repeating UI updates.

Why do we use requestAnimationFrame?

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.

What is timestamp in requestAnimationFrame?

The time stamp is: current time for when requestAnimationFrame starts to fire callbacks.


1 Answers

It's basically its own thing. When the browser is about to repaint the page, which it does typically 60 times/second if not blocked by a running task, it will call all queued requestAnimationFrame callbacks just before doing so, and then do the repaint.

The spec doesn't say whether that can happen between the completion of a task (macrotask) and the processing of its scheduled microtasks, or only between (macro)tasks. So presumably that can vary browser to browser.

The old spec (now obsolete and superceded) described it in (macro)task terms, suggesting it would be between (macro)tasks, but things may have moved on from there.

The spec now says when this happens in the Event Loop Processing Model section. The shortened version with a lot of detail removed is:

  1. Do the oldest (macro) task
  2. Do microtasks
  3. If this is a good time to render:
    1. Do some prep work
    2. Run requestAnimationFrame callbacks
    3. Render

Let's do a test:

const messages = [];
setTimeout(() => {
  // Schedule a microtask
  Promise.resolve().then(() => {
    log("microtask");
  });
  
  // Schedule animation frame callback
  requestAnimationFrame(() => {
    log("requestAnimationFrame");
  });
  
  // Schedule a (macro)task
  setTimeout(() => {
    log("(macro)task");
  }, 0);
  
  // Schedule a callback to dump the messages
  setTimeout(() => {
    messages.forEach(msg => {
      console.log(msg);
    });
  }, 200);

  // Busy-wait for a 10th of a second; the browser will be eager to repaint when this task completes
  const stop = Date.now() + 100;
  while (Date.now() < stop) {
  }
  
}, 100);

function log(msg) {
  messages.push(Date.now() + ": " + msg);
}

Sure enough, the results vary by browser:

  • Chrome: microtask, requestAnimationFrame, (macro)task
  • Firefox: microtask, (macro)task, requestAnimationFrame

(I reliably get the same results in repeated tests on those browsers. I don't have Edge handy...)

Now Chrome (and so presumably Chromium, Brave, and the new Edge), Firefox, iOS Safari, and Legacy Edge all do the same thing, which matches the spec: microtask, requestAnimationFrame, (macro)task.

Here's a version with the busy-wait up front, instead of at the end, in case it changes something:

const messages = [];
setTimeout(() => {
  // Busy-wait for a 10th of a second; the browser will be eager to repaint when this task completes
  const stop = Date.now() + 100;
  while (Date.now() < stop) {
  }
  
  // Schedule a microtask
  Promise.resolve().then(() => {
    log("microtask");
  });
  
  // Schedule animation frame callback
  requestAnimationFrame(() => {
    log("requestAnimationFrame");
  });
  
  // Schedule a (macro)task
  setTimeout(() => {
    log("(macro)task");
  }, 0);
  
  // Schedule a callback to dump the messages
  setTimeout(() => {
    messages.forEach(msg => {
      console.log(msg);
    });
  }, 200);

}, 100);

function log(msg) {
  messages.push(Date.now() + ": " + msg);
}

I reliably get microtask, requestAnimationFrame, (macro)task on both Chrome and Firefox with that change. I get the same results now as with the earlier snippet.


**Q1: **

Even though the browser has no repaint work, the requestAnimationFrame's callback will be excuted at the refresh rate (default 60 per second).

Provided nothing is blocking.

**Q2: **

That sentence means exactly, and only, what it says: Your callback will be called (along with any requestAnimationFrame callbacks that are queued up) immediately prior to drawing a frame. It doesn't mean that a frame is necessarily drawn every 60th of a second — because the thread may be busy doing other things.

Those callbacks will not interrupt other tasks. Again: If other tasks have the main UI thread busy, it's busy, and the framerate suffers.

like image 88
T.J. Crowder Avatar answered Sep 27 '22 16:09

T.J. Crowder