Event Loop: microtasks and macrotasks
The execution flow of browser JavaScript relies on an event loop. It is essential for optimization and the correct architecture.
In this cover, you will first find the theory and then the practical applications.
Event Loop¶
The event loop has a straightforward concept. There exists an endless loop when the engine of JavaScript waits for tasks, runs them and then sleeps expecting more tasks.
The engine’s primary algorithm is as follows:
- Once there are tasks: executing them, beginning with the oldest one.
- Sleep till a task turns out, then get to 1.
However, the JavaScript engine doesn’t do anything most of the time. It only runs when a handler, script, or an event triggers.
The examples of tasks can be the following:
- Once an external script <script src="..."> loads, the task is executing it.
- Once the user moves the mouse, the task is dispatching the mousemove event, executing handlers.
- Once the time has come for setTimeout, the task is running its callback.
More examples can be counted.
Once the tasks are set, the engine handles them, expecting more tasks while sleeping.
A task may come while the engine is busy, but it will be enqueued. As illustrated above, when the engine is busy running a script, the user can move the mouse, leading to mousemove , and setTimeout can be due. Also note, that while the engine is executing a task, rendering will not happen. Only after the task is complete, the changes are painted in the DOM. In case a task lasts too long, the browser is not able to carry out other tasks. After a long time, it will alert the “Page Unresponsive” offering to kill the task. That takes place once there are multiple complex calculations or a programming error that can bring an infinite loop.
So, you got acquainted with the theory, now let’s see what happens in practice.
Splitting CPU-hungry Tasks¶
Imagine having a CPU-hungry task.
For instance, syntax- highlighting is considered CPU-hungry. For highlighting the code, it implements analysis, generates multiple colored elements, places them to the document.
When JavaScript engine is busy with highlighting, it can’t do any DOM-related tasks, process user events, and so on.
To avoid problems, you can split a large task into pieces.Start at highlighting 100 lines, then schedule setTimeout for the following 100 lines.
For illustrating that approach, let’s consider a function, which counts from 1 to 1000000000:
let t = 0;
let start = Date.now();
function countDate() {
for (let j = 0; j < 1e9; j++) {
t++;
}
console.log("Done in " + (Date.now() - start) + 'ms');
}
countDate();
After running the code above, the code will hang for a while.
The browser can even demonstrate “ the script takes too long” alert.
You have the option of splitting the job with nested setTimeout calls like this:
let t = 0;
let start = Date.now();
function countDate() {
do {
t++;
} while (t % 1e6 != 0);
if (t == 1e9) {
console.log("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(countDate); // schedule the new call
}
}
countDate();
After that, the browser interface will be completely functional.
In case a new side task turns out at the time the engine is busy with part 1, it will be queued and then run when the 1st part is completed.
The periodic returns to the event loop between the countDate executions provide time for the engine to react to other user actions.
The most interesting thing is that both of the options (with and without split) are comparable in speed.
For making them closer, you can initiate an improvement. For that purpose, it’s necessary to move the scheduling to the countDate() start, like this:
let t = 0;
let start = Date.now();
function countDate() {
// move scheduling to the beginning
if (t < 1e9 - 1e6) {
setTimeout(countDate); // new call scheduling
}
do {
t++;
} while (t % 1e6 != 0);
if (t == 1e9) {
cosnole.log("Done in " + (Date.now() - start) + 'ms');
}
}
countDate();
Running it will make you save time, as it takes significantly less time.
Progress Indication¶
The next advantage of splitting large tasks for browser scripts is that it’s possible to demonstrate progress indication.
Usually, the rendering takes place after the currently running code is complete.
On the one hand, it’s very convenient, as the function can create multiple elements, adding them one by one and changing their styles.
Let’s see a demo:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="progressId"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progressId.innerHTML = i;
}
}
count();
</script>
</body>
</html>
While splitting a large task into pieces with setTimeout, the changes are painted out between them. So, the code below will look better:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="divId"></div>
<script>
let i = 0;
function count() {
// do some hard work
do {
i++;
divId.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e7) {
setTimeout(count);
}
}
count();
</script>
</body>
</html>
Acting After the Event¶
Inside the event handler, you can delay some actions until the event has bubbled up and handled. It can be implemented by wrapping the code into zero delay setTimeout.
Let’s see an example where a custom menuOpen event is dispatched into setTimeout in a way that it takes place after the click event is handled completely:
menu.onclick = function () {
// ...create a custom event with the data of the menu item that was clicked
let customEvent = new CustomEvent("menuOpen", {
bubbles: true
});
// dispatch the custom event asynchronously
setTimeout(() => menu.dispatchEvent(customEvent));
};
You can learn more about it in chapter Dispatching custom events.
Macrotasks and Microtasks¶
Along with macro tests, there exist microtasks that come exclusively from your code.
As a rule, promises create them. The execution of .then/catch/finally transforms into a microtask.
There exists a unique queueMicrotask(func) function, which queues func for running the microtask queue.
Right after each macro task, the engine performs all the tasks from the queue of microtasks before running any other task or rendering something, and so on.
For example:
setTimeout(() => console.log("timeout"));
Promise.resolve()
.then(() => console.log("promise"));
console.log("code");
In the picture below, you can see a richer event loop where the order is from top to bottom. In other words, the script comes first, then microtasks, rendering, and so on.
All the microtasks should be completed prior to any other event handling or other actions.
It plays an essential role because it assures that the application environment is similar between the microtasks.
In case you want to execute an asynchronous function before the changes are rendered or new events are handled, you should schedule it using queueMicrotask(func).
Let’s take a look at an example similar to the previous one. But, in this example, we use queueMicrotask(func) rather than setTimeout :
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="divId"></div>
<script>
let t = 0;
function count() {
// do some hard work
do {
t++;
divId.innerHTML = t;
} while (t % 1e3 != 0);
if (t < 1e6) {
queueMicrotask(count);
}
}
count();
</script>
</body>
</html>
Summary¶
In this chapter, we took a detailed look at the event loop algorithm in JavaScript.
The algorithm is the following:
- Dequeuing and running the oldest one from the macro task queue.
- Implementing all the microtasks.
- If there are changes, rendering them.
- In case the macro task queue is empty, waiting until a macro task turns out.
- Going to step 1.
For scheduling a macro task a zero delayed setTimeout(f) is used.
For scheduling a new microtask queueMicrotask(f) is used.
0 Comments
CAN FEEDBACK
Emoji