The Main Thread : le fil d’exécution JavaScript du navigateur

Technique
Lecture 8 minutes
Exécution Javascript : Event Loop
Frédéric Pineau - Expert Web Performance
publié par

Frédéric Pineau
Directeur Technique

Le navigateur utilise un fil d’exécution JavaScript JavaScript est un langage de programmation dynamique principalement utilisé pour ajouter des fonctionnalités interactives aux pages web. Il permet de manipuler le DOM, de gérer des événements, et d'effectuer des requêtes asynchrones. principal pour exécuter ses tâches. Les développeurs Front-end Le front-end désigne la partie visible d'un site web ou d'une application avec laquelle l'utilisateur interagit directement. Il englobe l'interface utilisateur, le design, et les éléments interactifs, généralement créés avec HTML, CSS, et JavaScript. avertis (et surtout anglophones) le nomment religieusement « The Main Thread Un thread (ou fil d'exécution) est une unité de traitement au sein d'un programme qui permet d'exécuter plusieurs tâches en parallèle, améliorant ainsi l'efficacité et la réactivité des applications.  » car il est le seul et l’unique à s’exécuter. Ainsi il est responsable de l’exécution de JavaScript, de la gestion des interactions utilisateur et de la mise à jour du DOM Le DOM (Document Object Model) est une représentation en structure d'arbre d'un document HTML ou XML, permettant aux développeurs d'accéder et de manipuler dynamiquement les éléments d'une page web via des langages de programmation comme JavaScript. . Le fil principal effectue également la mise en page et le rendu (layout et painting). Rien que ça !

Du coup, cet article est l’occasion pour nous d’expliquer un peu comment fonctionne cet élément essentiel du navigateur qui nous pose bien souvent des soucis en termes de performance web.

Qu’est-ce que le « fil principal » ?

Le fil principal est un fil unique où la majorité du travail d’exécution JavaScript est effectuée. Ces tâches incluent :

  • Exécution de JavaScript : JavaScript est, par nature, monothreadé et s’exécute par défaut sur le fil principal.
  • Manipulation du DOM : les mises à jour qui ont lieu sur le DOM sont effectuées sur le fil principal.
  • Recalcul des styles : lorsque les styles d’un élément changent, ils sont recalculés et appliqués sur le fil principal.
  • Interactions utilisateur : lorsque l’utilisateur fait défiler une page, clique sur un bouton ou effectue une action à l’écran, tout cela est géré sur le fil principal.

Pourquoi un seul fil ? Pourquoi ne pas utiliser plusieurs fils d’exécution JavaScript ?

Lorsque l’on découvre que le navigateur effectue autant de tâches sur un seul fil, la première question qui vient à l’esprit est : pourquoi ne pas utiliser plusieurs fils d’exécution ? Aujourd’hui, nous disposons de matériels bien plus puissants qui pourraient le permettre.

Je me suis posé la même question, et il n’y a qu’une seule raison pour laquelle le navigateur exécute encore la majorité des tâches sur un seul fil. Bien que les navigateurs modernes utilisent maintenant les Web Workers, qui permettent d’exécuter des tâches en arrière-plan sans bloquer le fil principal, ces derniers sont destinés aux tâches qui ne nécessitent pas d’interaction directe avec l’utilisateur ou la page en elle-même. C’est ballot mais c’est comme çà.

D’autres technologies, comme SharedArrayBuffer, Atomics et WASM, permettent également d’atténuer ce problème, mais elles sortent du cadre de cet article.

La nature monothreadée de JavaScript

JavaScript est le langage du web et, comme nous le savons tous, il est conçu pour être monothreadé. Il a été créé à la fin des années 90 pour rester simple et éviter des complexités comme les conditions de course et les interblocages (deadlocks), qui sont difficiles à gérer, en particulier avec le DOM, et qui peuvent entraîner de nombreux bogues.

Le DOM n’est pas thread-safe. Il s’agit d’une structure arborescente hiérarchique et la synchronisation de ses tâches en mode multithread introduirait des complexités majeures (verrous multiples et mutex). Le modèle monothreadé simplifie ces opérations et garantit une exécution séquentielle, évitant ainsi les incohérences complexes.

Comment JavaScript gère-t-il les tâches asynchrones s’il est monothreadé ?

JavaScript utilise le une boucle d’événement ou Event Loop pour gérer les tâches asynchrones et offrir un comportement non bloquant. Cela permet à JavaScript de rester réactif malgré sa nature monothreadée. Examinons comment les choses fonctionnent sous le capot.

D’abord, nous devons comprendre l’Event Loop et la façon dont JavaScript gère les différentes tâches.

Si JavaScript est monothreadé, on pourrait conclure qu’il ne peut exécuter qu’une seule tâche à la fois. Mais ce n’est pas le cas ! JavaScript peut gérer des tâches asynchrones et effectuer des opérations d’E/S (entrée/sortie) de manière non bloquante. Comment cela fonctionne-t-il ?

Environnement d’exécution JavaScript

JavaScript utilise une structure de pile d’appels (Call Stack) basée sur le principe FILO (First In, Last Out – Premier Entré, Dernier Sorti).

  1. Premier Entré : lorsqu’une fonction est appelée, elle est ajoutée au sommet de la pile.
  2. Dernier Sorti : la fonction la plus récente (ajoutée en dernier) est exécutée en premier. Une fois terminée, elle est retirée de la pile.

Les files d’attente de tâches en JavaScript

JavaScript utilise également deux files d’attente importantes :

  1. La file d’attente des callbacks ( Callback Un callback est une fonction qui est passée en paramètre à une autre fonction et qui est appelée lorsque cette dernière a terminé son exécution, souvent pour gérer des opérations asynchrones. Queue) :
    • Basée sur FIFO (First In, First Out – Premier Entré, Premier Sorti).
    • Elle gère des opérations comme setTimeout, setInterval et les événements DOM.
    • L’Event Loop y prélève des tâches et les envoie à la pile d’exécution lorsque celle-ci est vide.
  2. La file des microtâches (Microtask Queue) :
    • Fonctionne aussi en FIFO, mais elle a une priorité plus élevée que la file d’attente des callbacks.
    • Elle gère des opérations comme Promise.then() et MutationObserver.
    • Avant de traiter la file des callbacks, l’Event Loop exécute toutes les tâches de la file des microtâches.

Cycle de fonctionnement de l’Event Loop

  1. Exécuter le code synchrone : l’Event Loop commence par exécuter tout le code synchrone dans la pile d’appels.
  2. Vérifier la file des microtâches : si elle contient des tâches, elles sont déplacées vers la pile et exécutées.
  3. Traiter la file des callbacks : une fois la file des microtâches vide, l’Event Loop prend les tâches de la file des callbacks et les exécute.
  4. Répéter indéfiniment : ce cycle se répète en permanence, permettant à JavaScript de gérer du code synchrone et asynchrone de manière fluide.

Exécution Javascript : Principe de l'event loop

Exemple d’exécution JavaScript

console.log("Début");

setTimeout(() => {
console.log("Tâche 1 (File des callbacks)");
}, 0); 

Promise.resolve().then(() => {

console.log("Tâche 2 (File des microtâches)");

}); 

console.log("Fin");

Résultat attendu :

Début
Fin
Tâche 2 (File des microtâches)
Tâche 1 (File des callbacks)

Maintenant que vous comprenez le fonctionnement de l’Event Loop, voyons comment le navigateur gère les tâches sur le fil principal.

Comment le navigateur travaille sur le fil principal ?

Une grande partie du travail du navigateur est effectuée sur le fil principal, notamment :

  • Exécution JavaScript : exécution du code synchrone et asynchrone.
  • Gestion des interactions utilisateur : traitement des événements comme les clics, le défilement et la saisie clavier.
  • Mise à jour du DOM : recalcul des styles, mise en page et rendu à l’écran.
  • Exécution de l’Event Loop : orchestration des files d’attente et mise à jour de l’affichage.

Exécution JavaScript

Tout le code JavaScript, qu’il soit synchronisé ou asynchrone, s’exécute sur le thread principal. Cela signifie que des scripts complexes, des boucles longues ou des setTimeout mal maîtrisés peuvent monopoliser le thread, bloquant ainsi l’exécution d’autres tâches cruciales, comme la réponse aux interactions utilisateur ou le rendu de l’interface.

Gestion des interactions utilisateur

Lorsqu’un utilisateur clique sur un bouton, fait défiler la page, tape au clavier, ou interagit d’une quelconque manière, ces événements sont captés puis traités sur le thread principal.

Le navigateur doit :

  1. Capturer l’événement.
  2. Exécuter les callbacks JavaScript associés (addEventListener, etc.).
  3. Appliquer les effets induits (ex. : changement d’état, animation, DOM modifié).

Si le thread principal est déjà occupé, la prise en compte de ces interactions sera retardée, ce qui crée une sensation de lenteur ou d’interface figée.

Mise à jour du DOM

Dès qu’un élément change (via le code ou une action utilisateur), le navigateur doit :

  1. Recalculer les styles : mise à jour du CSSOM ( CSS Le CSS (Cascading Style Sheets) est un langage utilisé pour décrire l'apparence et la mise en page des documents HTML, en définissant des styles comme les couleurs, polices, marges, et positionnements des éléments sur une page web. Object Model).
  2. Calculer le layout : positionnement et dimensions des éléments.
  3. Peindre les éléments : transformation en pixels affichés à l’écran.

Toutes ces étapes s’exécutent sur le thread principal.

Des modifications fréquentes et profondes du DOM peuvent entraîner de lourds recalculs, ralentissant le rendu global de la page (en particulier si les éléments affectés sont nombreux ou imbriqués profondément dans le DOM).

Exécution JavaScript de la boucle d’événements

La boucle d’événements (Event Loop) agit comme un chef d’orchestre : elle gère le passage des tâches de la file d’attente (callback queue, microtask queue) vers la pile d’exécution (call stack). Que ce soit des tâches synchrones ou des callbacks de Promises, setTimeout, etc., leur exécution passe par ce mécanisme… qui tourne lui aussi sur le thread principal.

Pourquoi tout cela est-il problématique ?

Ces quatre types de tâches sont toutes concurrentes pour le même thread. Cela signifie qu’un JavaScript mal optimisé ou un DOM trop complexe peut empêcher :

  • Les interactions de s’exécuter à temps.
  • Le rendu de s’afficher correctement.
  • Les animations de rester fluides.
  • Le chargement de certaines données en arrière-plan.

MilleCheck à la rescousse !

Quand on commence à analyser les fils d’exécution de ses pages web, notre outil d’analyse de page web, Millecheck.ai, peut s’avérer utile au travers de plusieurs fonctionnalités :

Testez votre Main-thread !

Et mesurer les temps d’exécutions de chacun de vos JS.

Tester gratuitement

Conclusion

Le fil principal du navigateur est comme la salle des machines d’un navire, où se déroulent de nombreuses tâches essentielles (navigation, maintenance, production d’énergie). L’Event Loop agit comme le gestionnaire de tâches, veillant à ce que tout soit exécuté dans un ordre logique et efficace.

Ainsi optimiser ce qui s’exécute sur le thread principal est fondamental pour la performance web. Cela implique :

Bien comprendre le fonctionnement du fil principal et de l’Event Loop permet d’optimiser les performances et d’éviter les blocages dans vos applications web.

Source : Understanding the main thread in the browser
Visuel généré à partir d’une intelligence artificielle.

Articles similaires