Les Microtasks
Ce que tu vas apprendre
- Ce qu'est la file des microtasks et ce qui la differencie des macrotasks
- Quelles APIs creent des microtasks (Promise.then, queueMicrotask, etc.)
- Pourquoi les microtasks sont drainee entièrement avant le prochain macrotask
- Le danger de la microtask starvation
Prerequisites
- Avoir lu l'article sur les macrotasks
- Connaitre les bases des Promises (on les approfondit dans l'article 06)
Si les macrotasks sont comme une file d'attente au supermarche -- tu prends un ticket, tu attends ton tour -- les microtasks, c'est plutot le VIP qui passe devant tout le monde. Chaque. Fois.
Et c'est exactement pour ca qu'elles sont puissantes et dangereuses a la fois.
La file des microtasks
La file des microtasks (ou "microtask queue") est une file séparée de celle des macrotasks. Et elle a une propriété fondamentale : elle est drainee entièrement avant que l'event loop passe au macrotask suivant ou au rendu.
Event Loop
[Macrotask] --> Execute --> Drain ALL microtasks --> Rendu
^ |
+-----------------------------------------------+
File macrotasks: [task1] [task2] [task3]
File microtasks: [micro1] [micro2]
^ ^
| |
+-- TOUTES executees avant task2
Les APIs qui creent des microtasks :
Promise.then()/.catch()/.finally()queueMicrotask(fn)MutationObserver(navigateur)process.nextTick()(Node.js -- techniquement pas une microtask mais se comporte pareil, avec priorité encore plus haute)
Promise.then créé des microtasks
Quand une Promise est resolved, ses handlers .then ne s'executent pas immédiatement. Ils sont places dans la file des microtasks.
javascriptconsole.log("1");
Promise.resolve().then(() => {
console.log("2");
});
console.log("3");
// Affiche : 1, 3, 2
Le .then est enregistre pendant l'exécution synchrone. Quand la call stack se vide (apres le console.log("3")), l'event loop draine les microtasks. Le callback affiche "2".
Ceci est vrai meme si la Promise est deja résolue. Promise.resolve() est immediate, mais le .then est toujours asynchrone.
javascriptconst p = Promise.resolve(42);
// p est DEJA resolue ici
p.then(val => console.log(val));
console.log("sync");
// Affiche : sync, 42
// Meme resolue, le .then est une microtask
queueMicrotask : la microtask explicite
ES2020 a ajoute queueMicrotask() pour créer des microtasks sans passer par une Promise :
javascriptconsole.log("A");
queueMicrotask(() => {
console.log("B");
});
setTimeout(() => {
console.log("C");
}, 0);
queueMicrotask(() => {
console.log("D");
});
console.log("E");
// Affiche : A, E, B, D, C
Pourquoi B et D avant C ? Parce que B et D sont des microtasks, et C est un macrotask. Toutes les microtasks sont drainee avant le prochain macrotask.
queueMicrotask est utile quand tu veux reporter l'exécution d'un bout de code "juste apres" le code synchrone courant, mais avant tout le reste. C'est plus propre que Promise.resolve().then(fn) pour ce cas d'usage.
MutationObserver
Dans le navigateur, MutationObserver observe les changements du DOM et notifie via des microtasks :
javascriptconst observer = new MutationObserver((mutations) => {
console.log("DOM modifie", mutations.length, "changement(s)");
});
observer.observe(document.body, { childList: true });
// Modifier le DOM
const div = document.createElement("div");
document.body.appendChild(div);
console.log("apres appendChild");
// Affiche : apres appendChild, DOM modifie 1 changement(s)
Le callback du MutationObserver est une microtask. Il s'exécuté apres le code synchrone courant, mais avant le prochain rendu. C'est parfait pour reagir aux changements DOM avant qu'ils soient peints a l'ecran.
process.nextTick (Node.js)
Dans Node.js, process.nextTick est exécuté avant les microtasks de Promise. C'est une file a part, encore plus prioritaire :
javascriptPromise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
queueMicrotask(() => console.log("microtask"));
// Affiche : nextTick, promise, microtask
Attends -- pourquoi promise avant microtask ? Parce que dans Node.js, l'ordre est :
process.nextTickqueue (priorité maximale)- Promise microtasks
queueMicrotaskmicrotasks (meme file que les Promises en pratique, mais l'ordre depend de l'enregistrement)
En réalité, promise et microtask sont dans la meme file. L'ordre final est celui d'enregistrement : promise a ete enregistree avant microtask.
La recommendation officielle de Node.js : préféré queueMicrotask a process.nextTick dans le nouveau code. nextTick existe pour des raisons historiques.
Le drainage complet
Voici le point le plus important de cet article. Les microtasks sont drainee complètement. Ca veut dire que si une microtask en créé une autre, cette nouvelle microtask sera aussi exécutée avant le prochain macrotask.
javascriptsetTimeout(() => console.log("macro"), 0);
queueMicrotask(() => {
console.log("micro 1");
queueMicrotask(() => {
console.log("micro 2");
queueMicrotask(() => {
console.log("micro 3");
});
});
});
// Affiche : micro 1, micro 2, micro 3, macro
Les trois microtasks s'executent AVANT le setTimeout. Meme si micro 2 et micro 3 n'existaient pas encore quand le drainage a commence.
Sur paltemps.fr, j'ai utilise ce mecanisme pour batir un système de validation en cascade : chaque étape de validation enregistre la suivante comme microtask, garantissant que toute la validation est terminee avant le prochain rendu.
Microtask starvation
Et voici le piège. Si tes microtasks continuent a en créer d'autres indefiniment, l'event loop ne passera jamais au macrotask suivant. Et le rendu ne se fera jamais.
javascript// NE FAIS PAS CA
function infinite() {
queueMicrotask(infinite);
}
infinite();
// L'event loop est bloquee pour toujours
// Pas de rendu, pas de setTimeout, pas de click handler
// Le navigateur finira par tuer l'onglet
C'est exactement comme un while(true), mais plus sournois parce que le code "a l'air" asynchrone. La call stack se vide entre chaque microtask, mais l'event loop ne peut jamais avancer a l'étape suivante.
Iteration de l'event loop :
Macrotask: [script initial] -> Execute
Microtasks: [infinite] -> Execute infinite
Microtasks: [infinite] -> Execute infinite (cree par la precedente)
Microtasks: [infinite] -> Execute infinite
... -> JAMAIS de rendu, JAMAIS de macrotask
Compare avec un setTimeout recursif qui, lui, ne bloque pas :
javascript// Ceci est OK
function loop() {
// faire du travail
setTimeout(loop, 0); // macrotask -> l'event loop respire
}
loop();
Résumé
- Les microtasks (Promise.then, queueMicrotask, MutationObserver) sont dans une file séparée des macrotasks
- Elles sont drainee entièrement entre chaque macrotask, y compris celles créées pendant le drainage
process.nextTick(Node.js) a priorité encore plus haute que les microtasks de PromisequeueMicrotaskest la facon propre de créer une microtask sans Promise- La microtask starvation bloque l'event loop aussi efficacement qu'un
while(true)
Precedent : Les Macrotasks | Suivant : Ordre d'exécution