Passer au contenu principal
RM
Retour au blog

Hidden classes, TurboFan, Ignition, garbage collection — comment V8 reste le roi des moteurs JavaScript.

Radnoumane Mossabely10 min read
Moteur V8 JavaScript
V8
JavaScript
Chrome
Performance
Engine
0 vues

TL;DR

  • V8, le moteur JavaScript de Chrome, domine depuis 2008 grace a un pipeline d'execution en deux etages : Ignition (interprete) puis TurboFan (compilation optimisee).
  • Les hidden classes et les inline caches permettent a JavaScript d'etre rapide malgre son typage dynamique.
  • Le garbage collector Orinoco utilise du marquage concurrent pour minimiser les pauses.
  • V8 tourne partout : Chrome, Node.js, Deno, Cloudflare Workers, Electron. Cette ubiquite auto-renforce sa domination.
  • JavaScriptCore (Safari) et SpiderMonkey (Firefox) sont techniquement solides mais n'ont pas le meme ecosysteme.

2008 : l'annee ou JavaScript est devenu rapide

Avant Chrome, JavaScript etait lent. Vraiment lent. Les moteurs de l'epoque (SpiderMonkey dans Firefox, JScript dans IE) interpretaient le code ligne par ligne sans optimisation serieuse. JavaScript etait considere comme un langage de script pour ajouter des animations et valider des formulaires. Personne n'imaginait faire tourner des applications complexes dans un navigateur.

Le 2 septembre 2008, Google lance Chrome avec V8. Le moteur compile directement JavaScript en code machine natif, sans passer par du bytecode interprete. Les benchmarks sont sans appel : V8 est 10 a 100 fois plus rapide que les moteurs concurrents sur certaines operations.

C'est ce moment precis qui a rendu possible Node.js (2009), puis tout l'ecosysteme JavaScript cote serveur. Sans V8, pas de npm, pas de React, pas d'Electron, pas de l'industrie telle qu'on la connait.

Dix-sept ans plus tard, V8 reste le moteur dominant. Pourquoi ?

Le pipeline : d'Ignition a TurboFan

Le V8 d'aujourd'hui ne ressemble plus a celui de 2008. Le pipeline d'execution a ete entierement reconstruit. Il repose sur deux composants principaux.

Pipeline V8 : le code passe par Ignition puis, si execute souvent, est optimise par TurboFan

Ignition est l'interprete. Il prend l'AST (Abstract Syntax Tree) produit par le parser et le compile en bytecode. Ce bytecode est compact et s'execute rapidement. L'avantage d'un interprete : le demarrage est instantane. Pas besoin d'attendre une compilation couteuse pour commencer a executer du code.

TurboFan est le compilateur optimisant. Quand Ignition detecte qu'une fonction est executee souvent (c'est du "code chaud"), il la passe a TurboFan. Celui-ci analyse le bytecode, collecte des informations de type ("cette variable est toujours un entier"), et produit du code machine hautement optimise.

Le genie du systeme, c'est le dialogue entre les deux. TurboFan fait des hypotheses ("cette variable est toujours un nombre"). Si l'hypothese se revele fausse a l'execution (quelqu'un passe une string au lieu d'un nombre), V8 effectue une "deoptimisation" : il jette le code machine optimise et revient au bytecode d'Ignition. Pas de crash, pas d'erreur. Juste un ralentissement temporaire.

Ce mecanisme permet a V8 d'etre rapide sur le code previsible (la majorite du code en production) tout en restant correct sur le code dynamique et imprevisible.

Hidden classes : pourquoi JavaScript peut etre rapide

JavaScript est un langage a typage dynamique. Un objet peut changer de forme a tout moment : tu peux ajouter ou supprimer des proprietes, changer le type d'une valeur. En theorie, ca devrait rendre l'acces aux proprietes tres lent, parce que le moteur ne sait jamais a l'avance ou se trouve une propriete en memoire.

V8 contourne ce probleme avec les "hidden classes" (ou "maps" dans la terminologie interne). L'idee : meme si JavaScript ne declare pas de types, la plupart du code cree des objets qui ont la meme forme. Tous les objets { x: 1, y: 2 } se ressemblent.

V8 attribue une hidden class a chaque forme d'objet. Quand tu crees { x: 1, y: 2 }, V8 cree une hidden class qui dit "x est a l'offset 0, y est a l'offset 4". Le prochain objet avec la meme forme reutilise la meme hidden class.

hljs javascript
// Ces deux objets partagent la meme hidden class
const p1 = { x: 1, y: 2 };
const p2 = { x: 5, y: 10 };

// Cet objet a une hidden class differente (ordre different !)
const p3 = { y: 2, x: 1 };

// Et celui-ci casse la hidden class de p1
p1.z = 3; // p1 passe a une nouvelle hidden class

Pourquoi l'ordre compte ? Parce que V8 cree les hidden classes de facon incrementale. Ajouter x puis y produit une chaine de transitions differente de y puis x. C'est contre-intuitif, mais c'est ce qui permet les inline caches.

Inline caches : la memoire du moteur

Les inline caches (IC) sont le complement des hidden classes. Quand V8 execute point.x, il doit trouver ou x est stocke en memoire. La premiere fois, il cherche dans la hidden class. Mais il memorise le resultat : "pour un objet avec cette hidden class, x est a l'offset 0".

La fois suivante, il va directement a l'offset 0 sans chercher. C'est un acces direct en memoire, aussi rapide que dans un langage compile statiquement.

C'est pour ca que les bonnes pratiques de performance JavaScript insistent sur la "forme constante" des objets :

hljs javascript
// Bon : tous les objets ont la meme forme
function createPoint(x, y) {
  return { x, y };
}

// Mauvais : formes differentes selon la condition
function createPoint(x, y, z) {
  const point = { x, y };
  if (z !== undefined) {
    point.z = z; // Casse la hidden class
  }
  return point;
}

// Mieux : toujours la meme forme
function createPoint(x, y, z) {
  return { x, y, z: z ?? null }; // Forme constante
}

Quand les inline caches sont "monomorphiques" (toujours le meme type d'objet), l'acces est quasi-instantane. Quand ils deviennent "megamorphiques" (plein de types differents), V8 abandonne l'optimisation et revient a une recherche generique, beaucoup plus lente.

Orinoco : un garbage collector qui ne fait pas de pauses

La gestion memoire automatique est un des avantages de JavaScript. Mais historiquement, le garbage collector (GC) etait aussi son talon d'Achille : des pauses de plusieurs millisecondes qui faisaient "jank" dans les animations et les interfaces.

Orinoco, le GC de V8, a resolu ca avec plusieurs techniques :

Marquage concurrent. Le GC marque les objets vivants en parallele de l'execution JavaScript, sur des threads separés. Le thread principal n'est pas interrompu pendant cette phase.

Balayage concurrent. Meme principe pour le balayage de la memoire. Les pages memoire liberees sont recyclees en arriere-plan.

Marquage incremental. Pour les cas ou le marquage concurrent ne suffit pas, V8 peut marquer par petits increments entre les frames d'animation, en restant sous le seuil de 1ms par pause.

Compactage parallele. Quand la memoire est fragmentee, V8 compacte les objets pour reduire la fragmentation. Cette operation utilise plusieurs threads en parallele.

Le resultat : sur une application web moderne, les pauses GC sont typiquement sous la milliseconde. Invisible pour l'utilisateur. C'est un exploit technique souvent sous-estime.

Isolates : pourquoi V8 est partout

V8 a un concept architectural qui explique son ubiquite au-dela du navigateur : les isolates. Un isolate est une instance V8 completement independante, avec sa propre heap memoire, son propre GC, ses propres contextes d'execution.

Deux isolates ne partagent rien. Pas de memoire, pas d'etat. C'est cette propriete qui rend V8 ideal pour des environnements multi-tenant :

  • Cloudflare Workers : chaque worker tourne dans un isolate V8. Le demarrage est quasi-instantane (pas de VM a lancer, pas de conteneur a creer), l'isolation est forte, et l'empreinte memoire est minime.
  • Deno : utilise V8 comme runtime, avec les isolates pour la securite (sandbox par defaut).
  • Node.js : un isolate par processus, avec la possibilite de creer des worker threads (chacun avec son propre isolate).
  • Electron : chaque fenetre a son propre isolate pour l'isolation de securite.

Les isolates demarrent en quelques millisecondes, contre des centaines de millisecondes pour un conteneur Docker et des secondes pour une VM. C'est ce qui a permis l'edge computing JavaScript : les fonctions serverless qui tournent sur des CDN proches de l'utilisateur.

JavaScriptCore et SpiderMonkey : les challengers

V8 n'est pas le seul moteur JavaScript performant. Les deux autres principaux meritent qu'on en parle.

JavaScriptCore (JSC), le moteur de Safari et WebKit, utilise un pipeline a quatre niveaux : LLInt (interprete low-level), Baseline (compilation rapide), DFG (optimisation de graphe de flux de donnees), et FTL (optimisation maximum). C'est un pipeline plus gradue que V8, et JSC est souvent plus rapide sur le demarrage a froid. Sur iOS, c'est le seul moteur autorise (meme Chrome sur iOS utilise JSC, pas V8).

SpiderMonkey, le moteur de Firefox, est le plus ancien moteur JavaScript (cree par Brendan Eich en 1995). Il utilise WarpMonkey pour l'optimisation et a ete le premier a implementer certaines features comme le JIT tiering. SpiderMonkey est aussi tres bon sur la conformite aux standards.

Pourquoi n'ont-ils pas detrone V8 ? Pas parce qu'ils sont techniquement inferieurs. Sur certains benchmarks, JSC ou SpiderMonkey gagnent. La raison est plus structurelle :

  • Part de marche de Chrome (~65%). Plus d'utilisateurs = plus de sites optimises pour V8 = plus de raisons d'utiliser Chrome.
  • Ecosysteme Node.js/Deno/Bun. V8 est le runtime cote serveur de reference. Ca cree une boucle de renforcement : les devs optimisent pour V8, les outils ciblent V8, les nouveaux projets choisissent V8.
  • Investissement Google. L'equipe V8 est l'une des plus grandes equipes de moteur de langage au monde. Le budget R&D est enorme.

C'est un cas classique de "winner takes most" : le meilleur moteur n'est pas necessairement celui qui gagne, c'est celui qui a le plus gros ecosysteme.

Ce que ca veut dire pour les developpeurs

Comprendre V8 n'est pas un exercice academique. Ca a des implications directes sur comment tu ecris du JavaScript :

  1. Garde des formes d'objets constantes. Les hidden classes et inline caches te recompensent pour la regularite. Initialise toutes les proprietes dans le constructeur.

  2. Evite les megamorphismes. Si une fonction recoit 10 types d'objets differents, V8 ne peut pas optimiser. Prefere des fonctions qui travaillent sur des types homogenes.

  3. Laisse TurboFan faire son travail. Le code previsible (pas de eval, pas de with, pas de proprietes ajoutees dynamiquement) se compile beaucoup mieux.

  4. Ne t'inquiete pas du GC. Orinoco gere. Les micro-optimisations de memoire (object pooling, pre-allocation) sont rarement necessaires en JavaScript moderne.

  5. Les isolates changent l'architecture. Si tu construis un service multi-tenant, penser en termes d'isolates (Cloudflare Workers, Deno Deploy) peut etre plus pertinent que des conteneurs.

V8 n'a pas ete detrone parce qu'il est techniquement excellent et parce que son ecosysteme est auto-renforçant. Ce n'est pas pres de changer.

Ressources

Partager: