What are microtasks?
Many JavaScript developers might know about the event loop, about the call stack, and, most probably, even about the task queue.
But there's not a very high chance that they know about microtasks as well.
Do you know about microtasks? Well, let's learn about them.
As the name suggests:
Essentially, a microtask represents a task that is not that high-priority, or perhaps even feasible, to be executed alongside the code that is currently being executed on the call stack.
But at the same time, it's also not that low-priority to be executed after the page render, upon the next run of the event loop.
Akin to an asynchronous function's callback, such as the one provided to setTimeout()
, a microtask isn't executed immediately in the call stack. Instead, it's executed when the code currently being executed in the call stack reaches completion.
Tasks vs. microtasks
While it may seem that a microtask is just a special kind of a task in JavaScript, that's not completely true.
In fact, in the terminology of JavaScript, the term 'task' is exclusively used to represent any piece of code that is lined up in the task queue and, likewise, executed with every subsequent iteration of the event loop.
In contrast, the term 'microtask' is used to refer to any piece of code that is lined up in the microtask queue (more on that later below) and, likewise, executed after the completion of the current code in the call stack and before the next iteration of the event loop.
So in that sense, a microtask is truly NOT a task, per se.
Often times, in order to make this distinction between tasks and microtasks crytsal clear, the term 'macrotask' is used to denote what is otherwise denoted by the term 'task'.
Besides this, microtasks don't require any setup to be done for them in the background, such as maybe counting a timer, or dispatching an HTTP request. They're directly dumped into the microtask queue.
Tasks (i.e. the ones that are dumped into the task queue), however, typically do require some background setup.
For example, in the case of a callback function (which denotes a task) given to setTimeout()
, a timer is managed in the background, and once it completes, the callback is handed over to the task queue.
Moving on, JavaScript leverages microtasks mainly in two of its APIs:
- Promises, which we shall learn later on in this course, in the Advanced JavaScript — Promises unit.
- The
MutationObserver
API, used to observe for mutations happening in the DOM.
It also exposes a queueMicrotask()
function to be able to queue a function in the microtask queue. We shall explore this function in detail later in this chapter.
The microtask queue
All microtasks line up in a specialized queue, made solely for them — the microtask queue.
Here's an illustration of the microtask queue amongst the other integral components of the event loop:
Notice a couple of things here:
- The rectangle representing the microtask queue is smaller in size than the one for the task queue. This is indicative of the fact that the microtask queue is, more or less, not used as much as the task queue.
- The arrows signify the fact that web APIs populate the task and microtask queues, which then pass on their items to the call stack for execution.
- The task queue doesn't know about the microtask queue, and vice versa; each of them only interfaces with the call stack.
There's one particularly important thing to note regarding the microtask queue, in comparison to the task queue:
In contrast to this, the task queue is processed in turns — the first iteration of the event loop processed the first task in the task queue, the second iteration processed the second task, and so on and so forth.
This has an immediate consequence: queueing a microtask in a microtask itself results in that queued microtask to be also processed before the next run of the event loop.
This also means that an ill-defined microtask, i.e. one that continuously queues another microtask, can result in an indefinite loop over the microtask queue as long as it doesn't become empty (which wouldn't ever happen in this case).
Anyways, up until the advent of the queueMicrotask()
function in JavaScript, there wasn't any direct way to put anything into the microtask queue.
Sure, promises did internally used microtasks, and people relied on using promises to indirectly work with the microtask queue, but this was more or less a shim. However, queueMicrotask()
changed this.
In the next section, we shall see what queueMicrotask()
is and how to use it.
The queueMicrotask()
function
For quite a while, JavaScript had no standard way to set up microtasks. Developers relied on using promises as a clever trick to use microtasks but this approach wasn't without its issues.
For instance, any errors arising in the microtask functions would result in rejected promises instead of resulting in regular, thrown exceptions. Moreover, creating promises had an overhead of its own.
In short, while it was possible to work with microtasks via promises, it wasn't really efficient to go this way.
Fortunately, soon enough, a standardized function was introduced into JavaScript to be able to directly work with microtasks — queueMicrotask()
.
Let's investigate more about this function.
queueMicrotask()
function is used to literally queue a microtask on the associated event loop's microtask queue.As we already know from the discussion above, microtasks are executed after the current code in the call stack exits and before the next run of the event loop.
queueMicrotask()
accepts just a single argument, as described below:
queueMicrotask(callback)
callback
is the callback to be executed as a microtask.
Simple, isn't it?
Let's now consider a couple of examples of queueMicrotask()
.
In the following code, we make a couple of logs, followed by an invocation of queueMicrotask()
:
console.log('Before');
queueMicrotask(function() {
console.log('Microtask');
});
console.log('After');
Can you guess the output of this code?
Well, let's see it for real:
As can be seen, 'Before'
is output before 'After'
, and finally comes the text 'Microtask'
. This confirms that the callback passed to queueMicrotask()
is executed after the current code (where the function is called) reaches completion itself.
Let's extend this example with a setTimeout()
call. Consider the following code:
console.log('Before');
setTimeout(function() {
console.log('Timeout');
}, 0);
queueMicrotask(function() {
console.log('Microtask');
});
console.log('After');
Can you guess the output now?
Well, here it is:
As before, first we have 'Before'
, followed by 'After'
, indicating the synchronous execution of these statements. Next comes the text 'Microtask'
, and finally the text 'Timeout'
.
This might come as a surprise to anyone lacking a solid foundation of microtasks in JavaScript but, hopefully, not to you. Right?
Microtasks are executed immediately one-by-one after the current code's execution completes (in the call stack). Once all the microtasks are executed, only then is the event loop allowed to proceed with its next run where it dequeues a task from the task queue and executes it on the call stack.
This is the precise explanation of why 'Timeout'
gets logged after 'Microtask'
in the code above, despite the fact that the call to setTimeout()
comes before the call to queueMicrotask()
.
Simple? It's now time for a quick test for you.
What would be the order of logs of the following code?
console.log('Before');
setTimeout(function() {
console.log('Timeout');
}, 0);
queueMicrotask(function() {
console.log('Microtask 1');
queueMicrotask(function() {
console.log('Microtask 2');
});
});
console.log('After');
- Before After Microtask 1 Timeout Microtask 2
- Before After Microtask 1 Microtask 2 Timeout
queueMicrotask()
in line 9 (from within a microtask) results in a microtask placed in the microtask queue. Before the next iteration of the event loop, when eventually setTimeout()
's callback is entertained, this microtask is processed, thereby logging 'Microtask 2'
before 'Timeout'
.