Retour au cours

frontend / javascript

Internals V8 : JIT, hidden classes et optimisations

Leçon 241 exercice

Explication

Ce que vous allez apprendre

  • Décrire le rôle d'Ignition (interpréteur) et de TurboFan (compilateur JIT) dans V8
  • Expliquer ce que signifie "code chaud" et pourquoi il est recompilé en cours d'exécution
  • Comprendre pourquoi une déoptimisation a un coût réel de performance
  • Définir ce qu'est une hidden class et pourquoi la structure d'un objet influence sa vitesse
  • Écrire des objets dont la structure reste stable pour rester dans le chemin optimisé du moteur

Dans quel contexte ?

Une fonction de traitement de commandes, exécutée des millions de fois par jour, devient soudainement deux fois plus lente après un déploiement, sans qu'aucune ligne de logique métier n'ait changé. En profilant avec les DevTools, l'équipe remarque que la fonction crée parfois l'objet commande avec les propriétés dans l'ordre { id, montant, statut } et parfois { id, statut, montant }, selon un chemin conditionnel ajouté récemment. Cette variation d'ordre casse l'optimisation "hidden class" de V8, forçant le moteur à traiter ces objets de façon générique et bien plus lente.

1. Un changement de niveau d'abstraction

Cette leçon change radicalement de niveau : au lieu d'apprendre une fonctionnalité du langage, elle explique ce qui se passe entre le moment où vous écrivez du JavaScript et celui où le processeur l'exécute réellement, en prenant V8 comme exemple.

Prérequis

Cette leçon a une saveur plus avancée : elle suppose une bonne aisance avec les objets (leçon 4) et prolonge directement les conseils de performance déjà abordés à la leçon 19 sur la mémoire.

2. D'abord, un interpréteur rapide à démarrer

Votre code est d'abord transformé en bytecode et interprété par Ignition, un module conçu pour démarrer vite, quitte à ne pas être le plus rapide en exécution continue.

3. Ensuite, un compilateur qui optimise le code "chaud"

Le code exécuté très fréquemment est repéré et recompilé à la volée par TurboFan, un compilateur JIT qui produit du code machine spécialisé et bien plus rapide — mais seulement sous certaines hypothèses sur les données manipulées.

Composant V8RôleVitesse de démarrageVitesse en régime établi
IgnitionInterprète le bytecodeRapideModeste
TurboFanCompile le code "chaud" en code machineLent (compilation à la volée)Très rapide

4. Le prix à payer si ces hypothèses sont fausses

Si ces hypothèses sont violées en cours de route, TurboFan doit "déoptimiser" et revenir temporairement à l'interprétation, un coût de performance réel.

5. Le concept central : les hidden classes

Deux objets créés de la même façon, avec les mêmes propriétés ajoutées dans le même ordre, partagent une structure interne commune que V8 peut fortement optimiser.

Piège fréquent

Créer un objet puis ajouter des propriétés dans un ordre différent selon une branche conditionnelle (if (cas) { obj.a = 1; obj.b = 2; } else { obj.b = 2; obj.a = 1; }) génère deux hidden classes distinctes pour des objets "équivalents", empêchant V8 d'appliquer ses optimisations les plus efficaces.

6. Ce qui casse cette optimisation

Ajouter des propriétés dans un ordre variable selon les cas crée des formes différentes, forçant le moteur à abandonner ses optimisations spécifiques.

Cette leçon relie directement des conseils déjà croisés à la leçon 19 (structure d'objet stable) à leur véritable justification technique : ce ne sont pas des superstitions, mais des conséquences mesurables du fonctionnement interne du moteur.

Commandes & code

Internals V8 : niveau expert

Comment le moteur JavaScript de Chrome/Node.js exécute réellement le code.

js
// --- Pipeline d'execution de V8 : parse -> Ignition (interpreteur) -> TurboFan (JIT) ---
// 1. Le code source est parse en AST (Abstract Syntax Tree)
// 2. Ignition genere et execute un bytecode interprete (demarrage rapide)
// 3. Le code "chaud" (execute frequemment) est compile a la volee par TurboFan (JIT)
//    en code machine optimise -- bien plus rapide que le bytecode interprete
// 4. Si les hypotheses d'optimisation sont violees, TurboFan "deoptimise" (retour a Ignition)

// --- Hidden Classes (Shapes) : comment V8 optimise l'acces aux proprietes d'objet ---
function Point(x, y) {
    this.x = x;      // cree la hidden class C0 -> C1 (ajout de "x")
    this.y = y;       // C1 -> C2 (ajout de "y")
}
const p1 = new Point(1, 2);      // suit le chemin C0 -> C1 -> C2
const p2 = new Point(3, 4);       // suit le MEME chemin -> partage la hidden class C2 avec p1
// V8 peut alors generer du code machine SPECIALISE pour cette forme d'objet precise

// PIEGE : ajouter des proprietes dans un ordre different cree des hidden classes DIFFERENTES
function creerPointIncoherent(x, y, ajouterZAvant) {
    const p = {};
    if (ajouterZAvant) {
        p.z = 0;
        p.x = x;
        p.y = y;
    } else {
        p.x = x;
        p.y = y;
    }
    return p;
    // p.z avant p.x/p.y VS p.x/p.y seuls -> deux hidden classes DIFFERENTES
    // -> V8 ne peut plus reutiliser le meme code optimise -> "megamorphic" -> plus lent
}

// --- Inline Caches (IC) : cache par site d'appel pour accelerer les acces repetes ---
function obtenirX(objet) {
    return objet.x;      // ce SITE D'APPEL memorise la hidden class rencontree
}
// monomorphic (1 seule hidden class rencontree ici) : le plus rapide, V8 saute directement au bon offset
obtenirX(p1);
obtenirX(p2);
// polymorphic (2 a 4 hidden classes differentes) : toujours optimisable, mais plus lent
// megamorphic (5+ hidden classes) : V8 abandonne l'optimisation specifique, retombe sur une recherche generique

// --- Deoptimisation : quand TurboFan doit "annuler" une optimisation ---
function additionner(a, b) {
    return a + b;
}
for (let i = 0; i < 100_000; i++) additionner(1, 2);      // TurboFan optimise pour des NOMBRES

additionner("a", "b");
// L'hypothese "a et b sont toujours des nombres" est violee -> DEOPTIMISATION
// -> V8 retombe temporairement sur le bytecode interprete, plus lent, le temps de reoptimiser

// --- Arrays : "elements kinds" -- un autre systeme d'optimisation specifique aux tableaux ---
const tableauRapide = [1, 2, 3];              // PACKED_SMI_ELEMENTS : optimisation maximale
tableauRapide.push(4.5);                        // PACKED_DOUBLE_ELEMENTS : legerement moins optimal
tableauRapide.push("texte");                      // PACKED_ELEMENTS (generique) : encore moins optimal
tableauRapide[10] = 1;                              // HOLEY_ELEMENTS (tableau avec "trous") : le plus lent

// Bonne pratique : garder des tableaux homogenes en type, sans trous, pour rester dans le mode rapide
const tableauOptimal = new Array(1000).fill(0);     // pre-alloue, pas de "trous", type homogene

// --- Le tas (heap) et les generations : voir aussi la lecon "Performance et memoire" ---
// New Space (Scavenger, tres frequent, tres rapide) -> Old Space (Mark-Compact, moins frequent)

// --- Mesurer les optimisations reelles : --trace-opt / --trace-deopt (flags Node.js) ---
// node --trace-opt --trace-deopt script.js
// (affiche quand une fonction est optimisee/deoptimisee par TurboFan)

// --- Monomorphisme en pratique : conseil de conception d'API ---
class Vecteur {
    constructor(x, y) {
        this.x = x;       // TOUJOURS le meme ordre d'initialisation
        this.y = y;        // -> toutes les instances partagent la meme hidden class
    }
}
// Preferer des classes/constructeurs a des objets litteraux construits dynamiquement
// avec des proprietes conditionnelles, pour rester "monomorphic" et laisser V8 optimiser au maximum

Résumé

  • V8 interprète d'abord via Ignition, puis compile en JIT le code "chaud" via TurboFan pour un gain massif de performance.
  • Les "hidden classes" (shapes) permettent des accès aux propriétés en temps quasi-constant pour des objets de structure identique.
  • Un site d'appel monomorphic (une seule forme d'objet rencontrée) reste optimisé ; megamorphic (5+ formes) dégrade fortement la performance.
  • Garder des tableaux homogènes, sans trous, et une structure d'objet stable est le levier de performance le plus élevé côté moteur.

Exercices pratiques

1 disponible
1

Mission : traquer une fonction devenue megamorphic

Objectif : Diagnostiquer pourquoi une fonction de traitement de commandes casse les optimisations de V8, puis stabiliser sa forme d'objet.

Contexte

Une fonction traiterCommande(commande), appelée un million de fois par jour, ajoute parfois une propriété optionnelle après coup :

js
function creerCommande(id, montant, urgente) {
    const commande = { id, montant };
    if (urgente) {
        commande.note = "urgent";
    }
    return commande;
}

function traiterCommande(commande) {
    delete commande.temporaire;
    // ... traitement ...
    return commande;
}

Le profiling DevTools indique que le site d'appel dans traiterCommande est devenu "megamorphic".

Résoudre l’exercice →