Tasks, microtasks, queues and plans
I ’m bringing to your attention a translation of the article “Tasks, microtasks, queues and schedules” by Jake Achibald, who holds the position of Developer Advocate for Google Chrome.
When I told my colleague Matt Gant that I was thinking of writing an article about the sequence of microtasks and the order of their execution inside the browser event cycle, he said, “Jake, I'll be honest, I won’t read about it.” Well, I still wrote, so sit back and let's figure it out together, okay?
In fact, if it will be easier for you to watch the video, there is a wonderful performance by Philip Roberts at JSConf, which talks about the event cycle - it does not cover microtasks, but otherwise is an excellent introduction to the topic. In any case, they drove ...
Let's look at the following JavaScript code:
What do you think, in what order should the logs be displayed?
The correct answer:
Microsoft Edge, Firefox 40, iOS Safari and desktop Safari 8.0.8 log
For a more accurate understanding of the process, you must first imagine how the event cycle processes tasks and microtasks. For the first time, this may seem too complicated. Deep breath ...
Each “stream” has its own event loop, and therefore each web worker, so that they can be executed independently, while all windows from the same domain (according to the same origin rule) share the same event loop, because they can simultaneously communicate with each other. The event loop runs continuously, completing the queued tasks. Tasks are performed sequentially and cannot overlap. Okay, don’t leave ...
Tasks are planned in such a way that the browser can step out of their jungle to the ground of JavaScript / DOM and be sure that these actions occur in turn. Handling the mouse click event callback requires task scheduling, as does HTML parsing
Micro-tasks are usually planned for things that should be executed immediately after the current executable script. For example, responding to a bunch of actions or in order to do something asynchronously without having to lose performance from scratch due to a completely new task. A microtask queue is deployed at the end of each complete task, as well as after the callbacks if no other JavaScript is being executed. Any additional microtasks that are queued during deployment of the microtask queue are added to the end of the queue and are also processed. Micro-tasks include Mutation observer and Promises, as in the example above.
As soon as a promise is resolved, or if it has already been resolved, it queues the microtask to execute the callback. This gives confidence that promise calls are executed asynchronously even if they are already resolved. So, the challenge
Note translator: in this place, the author has in the original inserted a great visual presentation of the work of the JavaScript planner, however, I hardly have the technical ability to repeat this on Habré, for this I send the curious reader to the original page.
Yes, I really made a step-by-step animated diagram. How did you spend your Saturday, probably walked somewhere in the fresh air with friends? Well, I do not. In case something is not clear in my awesome UI, try clicking the left and right arrows.
They take in the journal
Although in this way we make the assumption that the implementation is correct, the only way is to test. See the log output order for promises and
The exact way is to look at the specification. For example, it
In the ECMAScript world, microtasks are called jobs. On
Now let's take a look at a more comprehensive example.In the hall, someone will cry embarrassedly, “No, they are not ready!” Do not pay attention, you are ready.
The following task might seem complicated to me before I wrote this post. Here is a small piece of HTML:
Logically, what will the following JavaScript code output to the log if I click
Try to think before you go to the answer. Hint: logs can be displayed more than once.
Note translator: the author at this point in the blog has an interactive DOM element (direct link) on which you can personally verify the behavior of your browser.
You thought it would be different? I hasten to reassure you, perhaps you were right. Unfortunately, different browsers have different degrees of acceptance of this opinion:
Handling the click event is a task. The Mutation observer and Promise callbacks are queued as microtasks. Kolbek
taking a look.) So Chrome behaves correctly. For me, the news was to find out that the micro-tasks are deployed after the callbacks (unless this is part of the execution of another JavaScript script), I thought that their deployment was limited only by the completion of the task. This rule is described in the HTML callback call specification:
Firefox and Safari correctly emptying the microtask queue between click handlers, as seen by mutation callbacks, but promises are queued differently. This could be forgiven, especially given the nebulous connection between the job ("jobs") and the microtask, but I expected that they would be executed between the handlers. Application for Firefox. Application for Safari.
We already realized that Edge queues promises incorrectly, but it also did not empty the queue of microtasks between click handlers, instead the queue was deployed only after calling all handlers, which explains the only conclusion
Pancake! But what if we add to the previous example:
The event will begin to be processed in the same way as before, but by means of a call from a script, and not from real user interaction.
Note translator: in the original there is another interactive platform where you can click a button and find out the correct answer for your browser (direct link).
And I do not stop getting various results in Chrome, I have updated this table a hundred times already thinking that before that I mistakenly checked in Canary. If you have other results in Chrome, tell me in the comments on which version you have.
Note translator: in this place, one more last time, the author gives us the opportunity to enjoy the visualization of the wonders of engineering of browser builders (link, again, direct).
Thus, the correct procedure is as follows:
After each of the click handlers is called ...
Still, it will eat you from the inside (UV). I came across this when I tried to create a concise wrapper over IndexedDB using promises instead of the awful IDBRequest objects. With it, IDB almost pleased me .
When a success event is triggered in the IDB, the transaction object becomes inactive after the transfer of control (step 4). If I create a promise that is resolved during the initiation of this event, the handlers should execute before step 4 while the transaction is still active, but this does not happen in any browser other than Chrome, which makes the library seem to be useless.
In Firefox, this can be dealt with, because polyphiles of promises, such as es6-promise, use Mutation observers for callbacks, which are nothing more than microtasks. Safari enters the race state with this fix, but the problem is most likely with their broken IDB implementation . Unfortunately, IE / Edge cannot be fixed right now, since mutation events do not occur after the callbacks.
One can only hope that in this matter we will someday be able to observe interchangeability.
Finally:
Has anyone stayed here? Hello ?! Hello?
When I told my colleague Matt Gant that I was thinking of writing an article about the sequence of microtasks and the order of their execution inside the browser event cycle, he said, “Jake, I'll be honest, I won’t read about it.” Well, I still wrote, so sit back and let's figure it out together, okay?
In fact, if it will be easier for you to watch the video, there is a wonderful performance by Philip Roberts at JSConf, which talks about the event cycle - it does not cover microtasks, but otherwise is an excellent introduction to the topic. In any case, they drove ...
Let's look at the following JavaScript code:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
What do you think, in what order should the logs be displayed?
The correct answer:
script start
, script end
, promise1
, promise2
and setTimeout
, but for the time being in order in different browsers often different. Microsoft Edge, Firefox 40, iOS Safari and desktop Safari 8.0.8 log
setTimeout
in front of promise1
and promise2
. Which is really strange, because Firefox 39 and Safari 8.0.7 worked correctly.Why it happens
For a more accurate understanding of the process, you must first imagine how the event cycle processes tasks and microtasks. For the first time, this may seem too complicated. Deep breath ...
Each “stream” has its own event loop, and therefore each web worker, so that they can be executed independently, while all windows from the same domain (according to the same origin rule) share the same event loop, because they can simultaneously communicate with each other. The event loop runs continuously, completing the queued tasks. Tasks are performed sequentially and cannot overlap. Okay, don’t leave ...
Tasks are planned in such a way that the browser can step out of their jungle to the ground of JavaScript / DOM and be sure that these actions occur in turn. Handling the mouse click event callback requires task scheduling, as does HTML parsing
setTimeout
from the example above. setTimeout
waiting for a given delay and then planning a new task for his callback. Therefore, it setTimeout
is displayed in the log after script end
, since logging script end
is part of the first task, and the output of the word setTimeout
is the second. Be patient, we are almost there, the most interesting is ahead ...Micro-tasks are usually planned for things that should be executed immediately after the current executable script. For example, responding to a bunch of actions or in order to do something asynchronously without having to lose performance from scratch due to a completely new task. A microtask queue is deployed at the end of each complete task, as well as after the callbacks if no other JavaScript is being executed. Any additional microtasks that are queued during deployment of the microtask queue are added to the end of the queue and are also processed. Micro-tasks include Mutation observer and Promises, as in the example above.
As soon as a promise is resolved, or if it has already been resolved, it queues the microtask to execute the callback. This gives confidence that promise calls are executed asynchronously even if they are already resolved. So, the challenge
.then(func)
at the resolved promise immediately puts the microtask in the queue. That is why it is promise1
also promise2
output to the log after script end
, because the current executable script must end before microtasks begin to be processed. promise1
and promise2
are displayed in the log before setTimeout
because micro-tasks are always deployed to the next big task. Note translator: in this place, the author has in the original inserted a great visual presentation of the work of the JavaScript planner, however, I hardly have the technical ability to repeat this on Habré, for this I send the curious reader to the original page.
Yes, I really made a step-by-step animated diagram. How did you spend your Saturday, probably walked somewhere in the fresh air with friends? Well, I do not. In case something is not clear in my awesome UI, try clicking the left and right arrows.
What is wrong in some browsers?
They take in the journal
script start
, script end
, setTimeout
, promise1
and promise2
. Callbacks of promises are executed after these setTimeout
. It seems that for promis callbacks a whole separate task is set up instead of a simple microtask. Such behavior can lead to performance problems when using promises, because callbacks can be undeservedly delayed until rendering and other things that are related to a big task. Here are the applications for fixing the anomaly in Edge and Firefox (translator's note: at the time of writing the translation in the application for Firefox, it turned out that only the 40th and 41st versions suffer from unexpected behavior, but starting from the 42nd anomaly does not play). The nightly builds of WebKit behave as expected, so I assume that soon Safari will return to the righteous path again.How to understand when tasks are used, and when - microtasks
Although in this way we make the assumption that the implementation is correct, the only way is to test. See the log output order for promises and
setTimeout
. The exact way is to look at the specification. For example, it
шаг 14 setTimeout
queues a task, while in the specification for fixing a mutation, step 5 creates a microtask. In the ECMAScript world, microtasks are called jobs. On
шаге 8.a спецификации PerformPromiseThen
to queue the microtask EnqueueJob
. Unfortunately, for the time being there is no explicit relationship between jobs and microtasks, however , one of the es-discuss mailing lists mentioned that they should use a common queue. Now let's take a look at a more comprehensive example.In the hall, someone will cry embarrassedly, “No, they are not ready!” Do not pay attention, you are ready.
Level 1: Boss Fight
The following task might seem complicated to me before I wrote this post. Here is a small piece of HTML:
Logically, what will the following JavaScript code output to the log if I click
div.inner
?// Придержим ссылки на эти элементы
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Послушаем изменения атрибутов внешнего
// элемента с классом outer
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// А вот и колбек…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …который мы повесим на оба элемента
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
Try to think before you go to the answer. Hint: logs can be displayed more than once.
Test
Note translator: the author at this point in the blog has an interactive DOM element (direct link) on which you can personally verify the behavior of your browser.
You thought it would be different? I hasten to reassure you, perhaps you were right. Unfortunately, different browsers have different degrees of acceptance of this opinion:
![]() | ![]() | ![]() | ![]() |
---|---|---|---|
|
|
|
|
Who is right?
Handling the click event is a task. The Mutation observer and Promise callbacks are queued as microtasks. Kolbek
setTimeout
is a task. (Note of the translator: here again, an interactive diagram explaining step by step the principle of operation of the code given earlier, I recommendtaking a look.) So Chrome behaves correctly. For me, the news was to find out that the micro-tasks are deployed after the callbacks (unless this is part of the execution of another JavaScript script), I thought that their deployment was limited only by the completion of the task. This rule is described in the HTML callback call specification:
If the stack of script settings objects is now empty, perform a microtask checkpoint... and the microtask checkpoint means nothing more than expanding the microtask queue, unless we are already deploying the microtask queue. And here is what the ECMAScript specification about jobs (“jobs”) tells us:
- HTML: Cleaning up after a callback , step 3
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty ...... although “can be” in the context of HTML is “must be”, i.e. "must".
- ECMAScript: Jobs and Job Queues
What did browsers misunderstand?
Firefox and Safari correctly emptying the microtask queue between click handlers, as seen by mutation callbacks, but promises are queued differently. This could be forgiven, especially given the nebulous connection between the job ("jobs") and the microtask, but I expected that they would be executed between the handlers. Application for Firefox. Application for Safari.
We already realized that Edge queues promises incorrectly, but it also did not empty the queue of microtasks between click handlers, instead the queue was deployed only after calling all handlers, which explains the only conclusion
mutate
after both click
in the log. This is mistake.Level 1 Boss Evil Brother
Pancake! But what if we add to the previous example:
inner.click();
The event will begin to be processed in the same way as before, but by means of a call from a script, and not from real user interaction.
Test
Note translator: in the original there is another interactive platform where you can click a button and find out the correct answer for your browser (direct link).
![]() | ![]() | ![]() | ![]() |
---|---|---|---|
|
|
|
|
Why is it different now?
Note translator: in this place, one more last time, the author gives us the opportunity to enjoy the visualization of the wonders of engineering of browser builders (link, again, direct).
Thus, the correct procedure is as follows:
click
, click
, promise
, mutate
, promise
, timeout
and the last timeout
, it seems, is that Chrome is working correctly. After each of the click handlers is called ...
If the stack of script settings objects is now empty, perform a microtask checkpointPreviously, this meant that microtasks would be executed between click handlers, however the explicit one
- HTML: Cleaning up after a callback , step 3
.click()
happens synchronously, so the script that called .click()
between the click handlers will still be on the stack. The above rule confirms that microtasks do not interrupt the execution of JavaScript code. This means that the microtask queue will not be deployed until all handlers are completed; the queue before microtasks will reach only after all event handlers.Is that really important?
Still, it will eat you from the inside (UV). I came across this when I tried to create a concise wrapper over IndexedDB using promises instead of the awful IDBRequest objects. With it, IDB almost pleased me .
When a success event is triggered in the IDB, the transaction object becomes inactive after the transfer of control (step 4). If I create a promise that is resolved during the initiation of this event, the handlers should execute before step 4 while the transaction is still active, but this does not happen in any browser other than Chrome, which makes the library seem to be useless.
In Firefox, this can be dealt with, because polyphiles of promises, such as es6-promise, use Mutation observers for callbacks, which are nothing more than microtasks. Safari enters the race state with this fix, but the problem is most likely with their broken IDB implementation . Unfortunately, IE / Edge cannot be fixed right now, since mutation events do not occur after the callbacks.
One can only hope that in this matter we will someday be able to observe interchangeability.
We did it!
Finally:
- Tasks are executed in order and the browser can render in between
- Micro tasks are executed in order and are executed:
- after each callback, unless that is part of the execution of some other script
- at the end of each task
Has anyone stayed here? Hello ?! Hello?