Boucle d'événements JavaScript, microtâches et macrotâches
Découvrez comment fonctionne la boucle d'événements JavaScript et comment les microtâches (Promises) et macrotâches (timers, événements) sont ordonnancées.
JavaScript exécute votre code sur un thread unique : une seule chose se produit à la fois, de haut en bas. Pourtant, il peut encore récupérer des données, exécuter des timers et répondre à des clics sans se figer. Le mécanisme qui rend cela possible est la boucle d'événements (event loop), et le travail qu'elle planifie est divisé en deux types de tâches : les microtâches et les macrotâches. Cette page explique ce qu'est chacune d'elles, l'ordre dans lequel elles s'exécutent et les pièges qui font trébucher les développeurs — chaque exemple ici est exécutable pour que vous puissiez vérifier le résultat vous-même.
Comment fonctionne la boucle d'événements
La boucle d'événements est le planificateur qui décide quel morceau de code s'exécute ensuite. Pour l'illustrer, vous n'avez besoin que de trois éléments :
- Pile d'appels (Call stack) — là où votre code s'exécute réellement. Les fonctions sont empilées lorsqu'elles sont appelées et dépilées lorsqu'elles se terminent. JavaScript exécute tout ce qui se trouve sur la pile jusqu'à la fin avant de faire quoi que ce soit d'autre ; c'est la règle run-to-completion.
- Tas (Heap) — la mémoire où vivent vos objets. Non directement impliqué dans la planification, mais c'est le troisième élément que les gens s'attendent à voir nommé.
- Files d'attente de tâches (Task queues) — le travail en attente qui attend que la pile soit vide. Il y en a deux : la file des macrotâches (timers, événements UI, E/S) et la file des microtâches (callbacks de Promise et
queueMicrotask).
Un tour de boucle d'événements ressemble à ceci :
- Exécuter la tâche courante sur la pile jusqu'à ce qu'elle soit complètement vide.
- Vider la totalité de la file des microtâches — y compris les microtâches ajoutées pendant le vidage.
- (Dans un navigateur) effectuer les mises à jour visuelles en attente.
- Prendre une seule macrotâche de la file des macrotâches et l'exécuter, puis retourner à l'étape 2.
L'asymétrie clé : après chaque macrotâche, le moteur vide toutes les microtâches, mais il ne prend qu'une seule macrotâche par tour de boucle. Cette règle unique explique presque toutes les surprises d'ordonnancement que vous rencontrerez.
Voici la démonstration la plus simple possible, en utilisant setTimeout pour planifier une macrotâche :
Dans cet exemple :
console.log('Start');s'exécute en premier, affichant "Start" dans la console.setTimeoutplanifie un callback à exécuter après au moins 1000 millisecondes. Il revient instantanément et ne bloque pas les lignes suivantes.console.log('End');s'exécute immédiatement, affichant "End".- Ce n'est qu'après la fin du script synchrone (et une fois le délai écoulé) que la boucle d'événements récupère le callback de
setTimeoutdans la file des macrotâches et l'exécute, affichant "Timeout Callback".
Le résultat est Start, End, puis Timeout Callback — le callback du timer attend même s'il a été écrit au milieu. Le callback de setTimeout est une macrotâche : il ne s'exécute qu'après la fin du script en cours et de toutes les microtâches en attente. C'est ce qui maintient la page réactive — le code synchrone n'a jamais à attendre un timer ou une requête réseau.
Microtâches vs. Macrotâches
Que sont les macrotâches ?
Une macrotâche (aussi appelée simplement une « tâche ») est une unité de travail autonome que le moteur récupère une fois par tour de boucle. Les sources courantes sont :
setTimeout/setInterval: des timers qui exécutent un callback après un délai ou de manière répétée.- Événements DOM : un gestionnaire
click,scrollouinput. - E/S : réponses réseau, lectures de fichiers et similaires.
Le moteur exécute exactement une macrotâche, puis vide toutes les microtâches, puis (dans le navigateur) peut effectuer un rendu, avant de prendre la macrotâche suivante. Les macrotâches ne s'exécutent donc jamais dos-à-dos sans que la file des microtâches soit vidée entre les deux.
Que sont les microtâches ?
Une microtâche est une courte tâche que le moteur souhaite terminer dès que l'unité de code courante se termine — avant de céder la place à la macrotâche suivante ou au rendu. Elles proviennent de :
- Callbacks de Promise : les fonctions passées à
.then(),.catch()et.finally(), ainsi que le corps d'une fonctionasyncaprès unawait. queueMicrotask(fn): une fonction intégrée qui planifie une fonction directement dans la file des microtâches.
La différence cruciale : après la tâche courante, le moteur vide la totalité de la file des microtâches avant de faire quoi que ce soit d'autre. Si une microtâche planifie une autre microtâche, celle-ci s'exécute également lors du même vidage — avant que la prochaine macrotâche n'ait son tour.
Exemples de code concrets
Exemple 1 : un timer est une macrotâche
Imaginons que vous vouliez afficher un message après 2 secondes. La ligne de planification s'exécute maintenant ; le callback est placé dans la file des macrotâches jusqu'à ce que le délai soit écoulé et que la pile soit libre.
Explication : setTimeout revient instantanément, donc les deux lignes console.log en dehors de lui s'exécutent d'abord. Le callback est une macrotâche qui ne s'exécute qu'après la fin du script synchrone et le déclenchement du timer. Dans un navigateur, vous mettriez généralement à jour le DOM dans le callback, par ex. document.getElementById('message').textContent = 'Hello there!';.
Exemple 2 : un callback de Promise est une microtâche
Le callback .then() d'une Promise résolue ne s'exécute pas en ligne — il est mis en file d'attente en tant que microtâche et s'exécute une fois que le code synchrone courant est terminé.
Explication : Le résultat est Before the promise, After the promise, puis Promise resolved (microtask). Même si la Promise est déjà résolue, son callback .then() attend dans la file des microtâches jusqu'à la fin du code synchrone — puis il s'exécute avant n'importe quel timer.
Priorité des microtâches et macrotâches
Les microtâches ont toujours une priorité plus élevée que les macrotâches. Après la fin du script courant, le moteur vide toutes les microtâches en attente avant de toucher à une seule macrotâche — même un setTimeout(..., 0) planifié en premier. Remarquez dans l'exemple ci-dessous que la Promise 2 chaînée, créée à l'intérieur d'une microtâche, s'exécute toujours avant les deux timers, car la file des microtâches est entièrement vidée avant que la boucle ne passe à la suite.
Résultat attendu :
Start
End
Promise 1
Promise 2
Timeout 1
Timeout 2Cela montre que les microtâches s'exécutent immédiatement après le code synchrone, même avant les timers planifiés pour le même moment. Cette priorité signifie que les mises à jour basées sur les Promise se stabilisent le plus tôt possible.
Un piège : la famine des microtâches
Étant donné que le moteur vide la file entière des microtâches avant la prochaine macrotâche ou un rendu, une microtâche qui continue à planifier d'autres microtâches peut tout bloquer — les timers ne se déclenchent plus et la page ne peut plus se repeindre. C'est ce qu'on appelle la famine des microtâches (microtask starvation) :
Les cinq microtâches s'exécutent toutes avant le callback de setTimeout, même si le timer a été planifié en premier. Dans une vraie application, une version non bornée de cette boucle gèlerait l'interface utilisateur. La solution consiste à diviser le travail de longue durée en macrotâches (par ex. setTimeout(..., 0)), ce qui permet à la boucle d'événements d'effectuer un rendu et de gérer des événements entre les morceaux.
Quand utiliser laquelle
- Optez pour les microtâches (Promises,
queueMicrotask) lorsque vous voulez que du code s'exécute dès que l'opération courante se termine, mais de manière asynchrone — comme réagir à des données juste après la résolution d'unfetch. - Optez pour les macrotâches (
setTimeout, découpage du travail sur plusieurs timers) lorsque vous souhaitez délibérément céder la place au navigateur pour qu'il puisse effectuer un rendu ou gérer des entrées utilisateur avant de continuer — par exemple, découper un calcul lourd pour que la page reste réactive.
Conclusion
La boucle d'événements exécute votre code synchrone jusqu'à la fin, puis vide toutes les microtâches, puis prend une macrotâche, et recommence. Les microtâches (callbacks de Promise, queueMicrotask) s'exécutent toujours avant la prochaine macrotâche (timers, événements, E/S). Intérioriser cette règle unique vous permet de prédire l'ordre exact d'exécution de n'importe quel code asynchrone.
Pour aller plus loin, continuez avec les Promises, le chaînage de Promises, async/await et le chapitre dédié aux microtâches. Pour les API de timers utilisées ici, consultez la planification avec setTimeout et setInterval.