Before jumping onto macro and micro-tasks, let's quickly see how the event loop works.
Event loop
An Event Loop in JavaScript is a constantly running process that keeps a tab on the call stack.
Checks if the call stack is empty
Takes a callback(or task) from the task queue and puts it inside the call stack
The callback gets executed and pops out of the call stack, and the call stack is empty now.
This loop is continued till the task queue is empty.
Tasks inside the task queue can be broadly classified into 2 categories, micro-tasks, and macro tasks
In this, we will first cover theoretical details on how things work and then move to code examples.
Micro Task
A microtask is a short function that is executed after the function or program which created it exits and only if the JavaScript execution stack is empty, but before returning control to the event loop being used by the user agent to drive the script's execution environment.
One thing to remember, the micro-task runs before the user agent has the opportunity to react to actions taken by the microtasks. Just remember this line, we will cover this in more detail later in the blog.
Examples: Promises, queueMicrotask, MutationObserver
Macro Task
Macro-task represents some discrete and independent work. These are executed always after the execution of the JavaScript code and the micro-task queue is empty.
Examples: setTimeout, setInterval, setImmediate, requestAnimationFrame
If the task queue is not empty and the call stack is empty, the event loop will give priority to the microtasks over macrotasks. If only all microtasks are done, the macro tasks are run.
In the meanwhile, if more microtasks are being added to the task queue, these additional microtasks are also added to the end of the micro-queue and are processed before any macro tasks. This is because event-loop will continue to call all microtasks until there are no microtasks remaining in the task queue.
That was the theory, now let's predict outputs based on our knowledge.
console.log(1);
setTimeout(() => {
console.log(2)
},0);
Promise.resolve().then(() => console.log(3));
console.log(4);
output will be
1
4
3
2
If you are correct then, Bravo! you have understood the topic well.
But if you are wrong, let's see, how the piece of code is executed under the hood line by line.
Line1 - console.log() is synchronous, so it moves to call stack and executed immediately. Prints 1
Line2 - setTimeout() is asynchronous, so it moves to the browser api. Browser api puts a timer on the callback which is the second argument of the setTimeout. As the timer is zero here, it moves to the task queue(if the timer was supposed 2000ms, then it will wait for 2000ms in the browser api environment and then moves to the task queue). This is a macro-task.
Line3 - Promise is asynchronous, so its callback function moves to the task queue. This is a micro-task.
Line4 - console.log() is synchronous, so it moves to call stack and executed immediately. Prints 4
Now the call stack is empty
As microtasks are given priority over macrotasks. So callback of promise moves to the call stack and executes first and Prints 3 in the console, even though macrotask is present before microtask in the task queue.
Now callback of setTimeout will move to call stack and executed. Prints 2
Just remember synchronous task > micro task > macro task
Now let's discuss the point which I told you to remember at the top.
The micro-task runs before the user agent has the opportunity to react to actions taken by the microtasks
While the execution of microtasks the control is not with the browser. This lets the given function run without the risk of interfering with another script's execution.
Now suppose the microtask takes too long to execute, the browser cannot process other tasks like user events(button click, scroll). So after a time, it raises an alert "Page Unresponsive". That happens when there are a lot of complex calculations or a programming error leading to an infinite loop.
Let me give you an example.
To demonstrate this approach, for the sake of simplicity, instead of micro-task, Let's take a function that counts from 1 to 1000000000
let i = 0;
let start = Date.now();
function count() {
// do a heavy job
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
If you run the code below, the engine will “hang” for some time. Try to click other buttons on the page – you’ll see that no other events get handled until the counting finishes.
Now let's split the counting using setTimeout()
let i = 0;
let start = Date.now();
function count() {
// do a piece of the heavy job (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // schedule the new call (**)
}
}
count();
Now the browser interface is fully functional during the “counting” process.
For every 1000000 counts code splits
1st run counts - 1 to 1000000
2nd run counts - 1000001 to 2000000
and so on
So when a new event appears when the engine is busy counting 1st run it gets queued and then gets executed when 1st run is done and before the 2nd run starts.
So here is a performance tip, if you have a big sync calculation to do and you can’t use a web worker, consider splitting the task into multiple tasks using setTimeout. That way browser will not become unresponsive!
That's it!!!😍 I hope this blog gives you a basic understanding of how macro and micro-tasks works.