slashhuang
slashhuang

Reputation: 51

is requestAnimationFrame belong to microtask or macrotask in main thread task management? if not, how can we categorize this kind of render side task

how react schedule effects? I made some test, it seems hooks is called after requestAnimationFrame, but before setTimeout. So, I was wondering, how is the real implementation of scheduler. I checked react source code, it seems built upon MessageChannel api. Also, how event-loop runs the macrotask sequence, for instance setTimeout/script etc.?

const addMessageChannel = (performWorkUntilDeadline: any) => {
    const channel = new MessageChannel();
    const port = channel.port2;
    channel.port1.onmessage = performWorkUntilDeadline;
    port.postMessage(null);
}
const Component1 = () => {
    const [value,] = useState('---NOT INITIALISED')
    requestIdleCallback(() => {
        console.log('requestIdleCallback---')
    })
    useEffect(() => {
        console.log('useEffect---')
    }, [])
    Promise.resolve().then(() => {
        console.log('promise---')
    })
    setTimeout(() => {
        console.log('setTimeout---')
    });
    addMessageChannel(()=> {
        console.log('addMessageChannel---')
    })
    requestAnimationFrame(() => {
        console.log('requestAnimationFrame---')
    })
    return <div>{value}</div>;
}
export default Component1

browser console result:

promise---
requestAnimationFrame---
addMessageChannel---
useEffect---
setTimeout---
requestIdleCallback---

Upvotes: 5

Views: 3592

Answers (1)

Kaiido
Kaiido

Reputation: 137084

2024 update

The specs have just been rewritten so that the update the rendering steps of a Window's event-loop is actually queued from a task.
So it can now be said that animation frame callbacks are called from a task, though they're still callbacks, and there will still be microtask between each callback and other callbacks before and after. And while the specs don't assess that, the update the rendering task still has a very special place in the event-loop with its own rendering task source. Now back to the original (amended) answer.


I'm not sure about the useEffect so I'll take your word they use a MessageChannel and consider both addMessageChannel and useEffect a tie.

First the title (part of it at least):

[Does] requestAnimationFrame belong to microtask or macrotask[...]?

Technically... neither(See heading "2024 update" section) . requestAnimationFrame (rAF)'s callbacks are ... callbacks.
Friendly reminder that there is no such thing as a "macrotask": there are "tasks" and "microtasks", the latter being a subset of the former.
Now while microtasks are tasks they do have a peculiar processing model since they do have their own microtask-queue (which is not a task queue) and which will get visited several times during each event-loop iterations. There are multiple "microtask-checkpoints" defined in the event-loop processing model, and every time the JS callstack is empty this microtask-queue will get visited too.
Then there are tasks, colloquially called "macro-tasks" here and there to differentiate from the micro-tasks. Only one of these tasks will get executed per event-loop iteration, selected at the first step.
Finally there are callbacks. These may be called from a task (e.g when the task is to fire an event), or in some particular event-loop iterations, called "painting frames".
Indeed the step labelled update the rendering is to be called once in a while (generally when the monitor sent its V-Sync update), and will run a series of operations, calling callbacks, among which our dear rAF's callbacks.

Why is this important? Because this means that rAF (and the other callbacks in the "painting frame"), have a special place in the event-loop where they may seem to be called with the highest priority. Actually they don't participate in the task prioritization system per se (which happens in the first step of the event loop), they may indeed be called from the same event-loop iteration as even the task that did queue them.

setTimeout(() => {
  console.log("timeout 1");
  requestAnimationFrame(() => console.log("rAF callback"));
  const now = performance.now();
  while(performance.now() - now < 1000) {} // lock the event loop
});
setTimeout(() => console.log("timeout 2"));
Which we can compare with this other snippet where we start the whole thing from inside a rAF callback:

requestAnimationFrame(() => {
  setTimeout(() => {
    console.log("timeout 1");
    requestAnimationFrame(() => console.log("rAF callback"));
  });
  setTimeout(() => console.log("timeout 2"));
});

While this may seem like an exceptional case to have our task called in a painting-frame, it's actually quite common, because browsers have recently decided to break rAF make the first call to rAF trigger a painting frame instantly when the document is not animated.
So any test with rAF should start long after the document has started, with an rAF loop already running in the background...


Ok, so rAF result may be non deterministic. What about your other results.

  • Promise first, yes. Not part of the task prioritization either, as said above the microtask-queue will get visited as soon as the JS callstack is empty, as part of the clean after running a script step.
  • rAF, random.
  • addMessageChannel, see this answer of mine. Basically, in Chrome it's due to both setTimeout having a minimum timeout of 1ms, and a higher priority of the message-tasksource over the timeout-tasksource.
  • setTimeout used to have a 1ms minimum delay in Chrome and a lower priority than message events, still it would not be against the specs to have it called before the message.
  • requestIdleCallback, that one is a bit complex but given it will wait for the event-loop has not done anything in some time, it will be the last.

Upvotes: 7

Related Questions