Deep dive dans les entrailles des JS engines: écrire du code qui rend V8 heureux

#javascript
# performance

Je vous emmène dans les entrailles de V8 pour comprendre comment optimiser au mieux tout le code javascript que vous écrivez à longueur de journées.

On y est. Je vais enfin vous parler des navigateurs et de V8.

Les navigateurs web sont ma passion depuis, pfiou… Et quand j’ai commencé à m’intéresser à comment fonctionne un navigateur, j’ai voulu comprendre comment les pages étaient chargées et du coup, qu’est-ce qui faisait tourner tout ce JS qu’on écrit à longueur de journées.

Pourquoi c’est intéressant (au-delà de la connaissance pour la connaissance) ?

  • Pour comprendre ce que fait un navigateur en général
  • Pour comprendre comment il le fait et surtout comprendre les différentes architectures (car tous les navigateurs ne fonctionnent pas de la même manière)
  • Plus particulièrement, comment tourne le JS dans nos navigateurs pour:
    • optimiser les performances côté navigateur
    • débuguer intelligemment
    • améliorer la sécurité du côté des navigateurs

Je mets quelques exercices “pratiques” ou choses à aller visualiser directement chez vous, je trouve ca plus fun et je vous encourage à ouvrir un navigateur pour tester et à créer un petit projet js sur node pour les tests V8.

1. Les bases: comment fonctionne un navigateur ?

Avant de nous plonger dans V8, je trouve important de revoir rapidement certaines bases, pour avoir quelques notions en tête (si vous savez comment marche un navigateur et ce qu'est un JS engine, passez directement à la section 2).

1.1 Processus et threads

Un navigateur, c’est une application sur votre ordinateur. Quand on lance un navigateur, il se passe ce qui se passe quand on lance n’importe quelle application: on utilise le processus et les threads de notre ordinateur, du GPU et du CPU (le processeur, aka le cerveau de votre ordi).

Exercice pratique

Ce qui est intéressant, c'est que les navigageurs n'ont pas d'architecture standard. Un peu comme il existe des applications différentes pour lire vos mails (mail, gmail, yahoo), il existe différentes applications de navigateur et non seulement ils sont différents entre eux, mais ils sont aussi différents dans le temps car il évoluent vite : certains ont un seul processus avec des threads différents, d’autres ont plusieurs processus avec des threads qui communiquent entre eux, etc.

Le + de Dre Drey

1.2 Web engine vs JS engine

Revenons à comment s’affiche une page:

  1. le navigateur fait sa requête (qui va chercher l’info soit sur les serveurs soit dans le cache)
  2. il faut commencer à parser le code html reçu
  3. il faut afficher le code html parsé et le js pour construire le DOM (Document Object Model, la structure de la page web)
  4. il faut appliquer le css
  5. il faut ajouter les médias (images, vidéos, etc.)

BAM ca fait une page web (presque).

web engine vs js engine

Je reviens donc sur la différence web engine et js engine comme le montre superbement bien ce graphique: le navigateur utilise un JS engine. Attention donc à ne pas mélanger les torchons et les serviettes comme dirait ma grand-mère, par exemple pour Chrome (promis, j’ai pas d’actions chez eux):

  • Chrome c’est le nom de l’application du navigateur.
  • Chromium c’est le nom du projet qui wrappe toute l’app
  • Chromium utilise un web engine: Blink.
  • Blink utilise V8 comme js engine (ou “moteur javascript”, dirait l’Académie francaise).

Le moteur javascript, c’est ce qui permet au code javascript de s’exécuter. C’est une des pièces du puzzle du navigateur, mais qui peut être utilisée ailleurs que dans un navigateur. V8 est une facon d’executer du code js, mais il en existe d’autres: SpiderMonkey (utilisé dans les navigateurs Firefox), Nitro utilisé chez Safari, etc. Tous ces moteurs sont écrits différemment et parfois dans des langages différents (donc avec des performances, des avantages, etc. différents). Et comme un js engine permet d’executer du code JS, il peut être utilisé dans plein d’autres choses qu’un navigateur: ainsi V8 est utilisé pour nodejs, dans VsCode, etc.

Ca serait très long de détailler toutes les architectures de tous les navigateurs avec les spécificités de tous les moteurs javascript. C’est pourquoi j’ai décidé de me concentrer plus particulièrement sur V8 pour cet article.

2. Pourquoi s'intéresser à V8 en particulier ?

Pourquoi s’intéresser à V8 plutôt que SpiderMonkey, donc ? Glad you ask ! V8 est vraiment intéressant :

  1. Parce qu'il été adopté massivement : c’est le moteur javascript le plus utilisé aujourd’hui car il est intégré dans le navigateur Chrome, mais depuis le départ c’est un projet “standalone” qui a sa propre API et peut-être utilisé dans d’autres projets: il est ainsi utilisé de manière indépendante par Nodejs, MongoDB, Electron (qui supporte VSCode par exemple), etc. Du coup, il a une influence massive sur tout l’ecosysteme javascript.
  2. Parce que sa documentation est publique : contrairement à d'autres moteurs, V8 a une doc technique extensive, accessible, avec des articles de blog détaillés par l'équipe elle-même. Ca m'a permis, moi noob de première classe, de creuser et de comprendre très vite.
  3. Parce que son architecture est fascinante : L'équipe ajoute régulièrement de nouveaux tiers d'optimisation (Sparkplug en 2021, Maglev en 2023), et c'est fascinant à suivre. Sa JIT est optimisée dans un pipeline à 4 tiers qui va grave nous intéresser tout le long de cet article.

Bon, en fait je vous mens un peu, car en soi le gros de l'architecture et la JIT ne sont pas des éléments ultra distinctifs aujourd'hui: tous les moteurs JavaScript modernes font du JIT. SpiderMonkey (Firefox) utilise plusieurs tiers JIT avec WarpMonkey, JavaScriptCore (Safari) a une architecture similaire à 4 tiers, et même l'ancien Chakra (Edge) le faisait. La compilation JIT n'est absolument pas distinctive de V8. Mais ca, combiné à la documentation publique et à l'adoption massive, fait que V8 est un excellent cas d'étude pour comprendre les moteurs JavaScript en général.

3. JIT ou la “I can do both” architecture

"Audrey c'est quoi cet acronyme que tu balances depuis tout à l'heure, JIT ?", me direz-vous ?

Javascript est normalement un langage interprété, mais en fait, il est aussi compilé. La JIT, just in time compilation, accélère l’exécution du code, car comme son nom l’indique, il combine de l’interpretation et de la AOT (ahead of time) compilation. En gros, ca veut dire que le moteur va “lire” le script javascript et faire un mix entre l’interpréter et le compiler en langage machine à la volée.

Ce “mix” a commencé en 2015-2016 avec le pipeline composé de TurboFan (le compilateur) et d'Ignition (l'interpréteur, initialement pour réduire la mémoire utilisée par les téléphones Android). En 2021-2023, ce pipeline d’origine Ignition-TurboFan s’enrichit de 2 compilateurs supplémentaires pour arriver à un pipeline à 4 tiers :

Ignition (interpréteur) → Sparkplug (baseline compiler) → Maglev (mid-tier compiler) → TurboFan (top-tier compiler)

Le + de Dre Drey

4. Le pipeline V8 en détails

Comment ca marche de faire à la fois de la compilation et de l'interprétation ?

  • V8 commence par "lire" le code : il le parse et il le valide. S’il y a des erreurs de syntaxes, c’est à ce moment qu'elles ressortent, par exemple.
  • il construit ensuite un AST avec ce code: un Abstract Syntax Tree (on y reviendra)
  • Ignition (l'interpréteur), compile l’AST en du bytecode et l’exécute ligne par ligne tout en collectant des informations sur les types d'objets qu'il rencontre: combien de fois certaines fonctions sont appelées, etc.

Le + de Dre Drey

  • Sparkplug (le compilateur de base depuis 2021): Quand une fonction devient "warm" (chaude, utilisée régulièrement), Sparkplug entre en jeu. Il compile environ 10x plus lentement qu'Ignition mais génère du code natif basique sans optimisation lourde. Sparkplug est un peu le "quick win" : il traduit presque directement le bytecode en code machine, sans trop se poser de questions. Ça donne déjà un bon boost de perf sans prendre trop de temps de compilation.
  • Maglev : le mid-tier compiler (2023): Maglev, ajouté en 2023 (Chrome M117), se situe entre Sparkplug et TurboFan avec une philosophie "good enough code, fast enough". Il compile environ 10x plus lentement que Sparkplug mais 10x plus vite que TurboFan. Maglev fait des vraies optimisations (il construit un graphe de flux, fait de l'inlining limité, etc.) mais sans être aussi agressif que TurboFan. C'est le sweet spot pour du code "assez chaud" mais pas critique.
  • Quand une fonction devient vraiment hot (utilisée massivement), TurboFan prend le relais. Il fait des optimisations agressives : inlining de fonctions, élimination de code mort, spécialisation de type (si V8 voit toujours des nombres, il génère du code spécifique aux nombres), optimisations basées sur les "shapes" d'objets (on y revient juste après)

Ce qui est pratique dans le JIT du coup, c’est que l’interprétation et la compilation communiquent: via deux grands moyens, la dé-optimisation et la recompilation.

En fait, on compile le code, on le run, on collecte des informations, on recompile avec ces infos par exemple sur le type, on se dit "ouh cette fonction est importante en fait, recompilons-la plus optimisée". Quand on fait ca, on part du principe que cette fonction va utiliser les mêmes types… mais c’est possible que non : du coup si cette fonction run sur d’autres types d’objets, on va faire entrer la déoptimisation

Si TurboFan optimise du code en supposant qu'une fonction reçoit toujours des nombres, et qu'à un moment on lui passe une string, boom : déoptimisation. V8 jette le code optimisé et retourne à un tier inférieur (Maglev, Sparkplug, ou Ignition). On en reparle plus bas

5. Comment V8 optimise le code

On arrive au coeur du sujet: comment V8 fait-il pour que du JavaScript dynamique tourne aussi vite ? V8 utilise plusieurs techniques d'optimisation pour accélérer l'exécution du code JavaScript. Voici quelques-unes des plus importantes, avec des exemples pratiques que vous pouvez tester dans votre navigateur :

5.1 Hidden classes: faire croire que Javascript a des classes

Dans votre tête, vous imaginez probablement un objet JavaScript comme un dictionnaire : { nom: "valeur", autre: 42 }. Et techniquement, c'est vrai... mais ce n'est pas comme ça que V8 les stocke.

Les recherches dans un dictionnaire sont relativement lentes. Pour rendre rapide l'accès aux propriétés, V8 fait semblant que JavaScript a des classes. Quand vous créez un objet, V8 crée une "hidden class" derrière les rideaux qui stocke les méta-informations sur la structure de l'objet

Regardons ce qui se passe :

function Point(x, y) {
  this.x = x;
  this.y = y;
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);

Étape par étape :

  1. Quand p1 est créé, V8 crée une hidden class vide (appelons-la C0)
  2. Quand this.x = x s'exécute, V8 crée une nouvelle hidden class C1 qui dit "j'ai une propriété x à l'offset 0"
  3. Quand this.y = y s'exécute, V8 crée C2 qui dit "j'ai x à l'offset 0 et y à l'offset 1"
  4. Quand p2 est créé, V8 réutilise la chaîne C0 → C1 → C2 : p1 et p2 partagent la même hidden class finale !

Ces "Hidden Classes", ou "shapes", forment des chaînes de transition dans le moteur JavaScript. L'objet commence sans propriétés (shape vide), puis le moteur transite vers une shape qui contient la propriété 'x', puis vers une autre shape qui contient 'x' et 'y'. V8 connait donc "la structure" de l'objet.

Pourquoi c'est génial ? Parce que si V8 sait que p1 et p2 ont la même hidden class (la même structure en gros), il sait que p1.x et p2.x sont au même endroit en mémoire. Pas besoin de faire une recherche : on va direct à l'offset 0.

Le problème :

const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
p2.z = 5;

p2 ne partage plus la même shape que p1. Les optimisations qui étaient basées sur "tous les Points ont la même structure" tombent à l'eau...

Exercice pratique

5.2 Inline caches: mémoriser les accès

L'inline caching est une technique d'optimisation où V8 observe que les appels répétés à la même opération tendent à utiliser le même type d'objets. V8 va donc "cacher" le résultat du lookup de propriété.

Imaginons ce code :

function getX(point) {
  return point.x;
}

getX(p1);
getX(p2);
getX(p3);

La première fois que getX est appelé :

  • V8 ne sait rien sur point
  • Il doit faire un lookup complet : trouver la hidden class de point, chercher où est x, etc.
  • Après cette première exécution, V8 enregistre le type/shape qu'il a vu et le résultat de sa recherche (comme l'offset de la propriété) dans l'inline cache.

Les fois suivantes :

  • V8 check : "est-ce que la hidden class de ce nouvel objet est la même que celle que j'ai vue la dernière fois ?"
  • Si oui : fast path -> V8 va direct à l'offset mémorisé, pas de lookup, c'est rapide.
  • Si non : slow path -> V8 doit refaire une recherche complète.

5.3 Les états d'un inline cache

Un inline cache (IC) peut devenir Monomorphic, Polymorphic, ou Megamorphic au fil du temps, selon combien de shapes d'objets différentes il rencontre:

  1. Monomorphic : l'IC n'a vu qu'une seule hidden class -> optimal -> accès rapide, code optimisé.
  2. Polymorphic : l'IC a vu 2 à 4 shapes différentes. V8 stocke un petit ensemble de hidden classes et leurs offsets associés -> pas optimal, moins rapide (un peu plus de checks à faire).
  3. Megamorphic : l'IC a vu 5+ shapes différentes -> performance cliff -> V8 abandonne le cache et retourne à la recherche complète à chaque fois

Exemple concret :

function getX(point) {
  return point.x;
}

// Monomorphic : tous les points ont la même shape
const goodPoints = [];
for (let i = 0; i < 1000000; i++) {
goodPoints.push({x: i, y: i \* 2}); // Toujours x puis y
}

// Polymorphic : shapes différentes !
const badPoints = [];
for (let i = 0; i < 1000000; i++) {
if (i % 2) {
badPoints.push({x: i, y: i _ 2}); // Shape A
} else {
badPoints.push({y: i _ 2, x: i}); // Shape B (ordre différent!)
}
}

Certains articles que j'ai lus parlent de différence x56 (l'article de Matteo Malvica par exemple). Je nuancerais un peu (je n'ai pas reproduit ces chiffres avec mes petits exemples) : peut-être que sur des classes complexes ou du code plus long/complexe, la différence est plus flagrante, mais il faut aussi savoir que V8 dans ses dernières versions a beaucoup amélioré la gestion des inline caches polymorphiques et megamorphiques, donc la différence de performance peut varier selon la version de V8 et le contexte d'exécution.

Exercice pratique

6. Le cycle optimisation/déoptimisation

On a vu que V8 optimise agressivement le code. Mais que se passe-t-il quand ses suppositions sont fausses ?

Quand votre code brise les suppositions de V8, en créant des objets avec des ordres de propriétés différents, en mélangeant des types dans des arrays, ou en utilisant des patterns dynamiques, etc. vous déclenchez un “performance cliff”. V8 est forcé de jeter le code machine optimisé dans un processus appelé déoptimisation et revenir à un tier inférieur.

Exemple classique :

function add(x, y) {
  return x + y;
}

// V8 voit toujours des nombres
for (let i = 0; i < 10000; i++) {
  add(i, i + 1);
}
// TurboFan optimise : "add fait de l'addition d'entiers"

// Puis vous mettez des strings
add("hello", "world"); // -> DÉOPTIMISATION`

V8 avait généré du code machine spécialisé pour l'addition d'entiers. Quand il reçoit des strings, il doit :

  1. Jeter le code optimisé
  2. Revenir au bytecode (ou Maglev/Sparkplug)
  3. Exécuter en mode "générique" (plus lent)

Les déoptimisations causent généralement des ralentissements de 2x à 20x selon le contexte. Dans des boucles serrées ou des chemins très hot, la pénalité peut être plus sévère (potentiellement 100x ou plus).

6.1 L'enfer de la déoptimisation

L'enfer de la déoptimisation arrive quand une fonction est optimisée et déoptimisée beaucoup pendant l'exécution. Après quelques cycles, V8 va marquer la fonction comme non-optimisable.

Imaginez :

  1. V8 optimise add() pour des nombres → TurboFan
  2. Vous passez une string → déoptimisation → retour à Ignition
  3. Vous repassez des nombres → V8 ré-optimise → TurboFan
  4. Vous repassez une string → déoptimisation
  5. ...

Après quelques cycles comme ça, V8 abandonne : "cette fonction est impossible à optimiser, je ne vais plus essayer". Et vous vous retrouvez coincé avec une fonctin add lente même si vous ne l'utilisez plus qu'avec des nombres...

Exercice pratique

6.2 Quelques "optimization killers"

Certains patterns JavaScript rendent V8 très malheureux. En voici quelques-uns :

1. L'opérateur delete

Quand vous utilisez delete, vous ne retirez pas juste une propriété. Vous changez complètement la structure de l'objet d'une façon que V8 ne peut pas optimiser avec une simple transition de hidden class. Utiliser delete force V8 à passer les propriétés de l'objet en mode Dictionary, où les propriétés sont stockées dans une structure de hash map lente.

// Pas ouf :
const cache = {};
cache.user1 = { name: "Alice" };
cache.user2 = { name: "Bob" };
delete cache.user1; // tout le cache passe en dictionary mode

// Mieux :
cache.user1 = undefined; // ou null

2. Propriétés ajoutées après initialisation

// Polymorphic nightmare
function Person(name) {
  this.name = name;
}
const p1 = new Person("Alice");
const p2 = new Person("Bob");
p1.age = 30; // p1 a une shape différente de p2
p1.city = "Paris"; // encore une autre shape

// Solution: en faire directement un monomorphic
function Person(name, age, city) {
  this.name = name;
  this.age = age || null;
  this.city = city || null;
}

Exercice pratique

3. Ordre des propriétés incohérent

On l'a vu avec l'exemple {x, y} vs {y, x}. L'ordre compte, je ne reviens pas la-dessus.

4. Try/catch (mais ça s'améliore)

Historiquement, avoir un try/catch dans une fonction empêchait TurboFan de l'optimiser. Les versions récentes de V8 ont beaucoup amélioré ça, mais ça reste un point de vigilance.

7. Comment écrire du code qui rend V8 heureux

Maintenant qu'on comprend les mécanismes, voici les règles d'or :

7.1 Gardez des shapes d'objets stables

Initialisez les propriétés d'objets dans le même ordre pour qu'une hidden class puisse être partagée. Toujours appelez les fonctions avec le même type d'objets pour que l'inline caching puisse être utilisé.

// V8 est heureux:
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

// V8 est heureux:
function createPoint(x, y) {
  return { x, y }; // toujours dans le même ordre
}

7.2. Évitez les types mixtes

// V8 est malheureux:
const mixedArray = [1, "deux", { trois: 3 }, null, undefined];

// V8 est heureux:
const numbers = [1, 2, 3, 4, 5];
const strings = ["un", "deux", "trois"];

7.3. Ne pas utiliser delete sur du code hot

On l'a dit : delete = dictionary mode = mort de la perf. Mettez undefined ou null à la place.

7.4. Initialisez toutes les propriétés

Même si certaines sont null au début. Ça fixe la shape une bonne fois pour toutes.

7.5. Écrivez du code "ennuyeux" et prévisible

V8 récompense le code prévisible et stable avec des shapes d'objets consistantes, et punit le code dynamique et imprévisible. Concentrez-vous sur écrire du JavaScript "ennuyeux" et monomorphic qui garde les compilateurs optimisants heureux

Le code qui change de type, qui ajoute des propriétés à la volée, qui mute des structures... c'est flexible, c'est "JavaScript-y", mais c'est lent. Le code ennuyeux, répétitif, prévisible ? C'est rapide.

8. Bonus final: le garbage collector de V8

On ne peut pas parler de V8 sans évoquer le Garbage Collector (GC), le système qui nettoie automatiquement la mémoire. Mais contrairement à ce qu'on pense, comprendre comment il fonctionne peut vous aider à écrire du code plus performant.

8.1 Jeunes et vieux objets

V8 divise son tas mémoire (heap) en deux générations principales : la Young Generation (où les nouveaux objets sont alloués) et la Old Generation (où les objets qui ont survécu plusieurs cycles de GC sont promus).

La Young Generation se divise elle-même en deux :

  • Nursery : les objets tout juste créés
  • Intermediate : les objets qui ont survécu un premier GC

Pourquoi cette séparation ? L'hypothèse générationnelle : la plupart des objets meurent jeunes. Pensez aux variables temporaires dans une boucle, aux objets retournés par une fonction, etc. Ils sont créés, utilisés, puis deviennent inutiles presque immédiatement.

8.2 Les deux collecteurs

V8 utilise deux garbage collectors : le Scavenger qui collecte fréquemment la young generation, et le Mark-Sweep-Compact qui collecte le heap complet incluant old et young generation.

Le Scavenger (Minor GC) :

  • Utilise un algorithme de "semi-space copying" : la young generation est divisée en deux moitiés (From-Space et To-Space). Pendant l'exécution, seule une moitié est utilisée pour allouer des objets. Lors d'une collection, les objets vivants sont copiés de From-Space vers To-Space WikipediaV8
  • Ultra-rapide car il ne traite que la young generation (petite, jusqu'à 16 MB)
  • Très efficace pour les objets short-lived car le coût est proportionnel au nombre d'objets vivants, pas à la taille totale du heap V8

Le Mark-Sweep-Compact (Major GC) :

  • Pour la old generation (objets long-lived)
  • Plus complexe : marque les objets vivants, balaie les morts, compacte la mémoire
  • Plus lent, mais moins fréquent

8.3 Orinoco : paralléliser pour réduire les pauses

Orinoco est le nom de code du projet GC de V8 (on est d'accord que ca fait cocorico ? Mais je m'égare...). Le Scavenger parallèle a réduit le temps total de GC young generation sur le thread principal d'environ 20%-50% selon les workloads selon plusieures sources ici et aussi ici.

Ce que ça signifie concrètement :

  • Le GC utilise plusieurs threads en parallèle
  • Certaines phases tournent en concurrence avec l'exécution du JavaScript
  • Les pauses ("stop-the-world") sont drastiquement réduites

8.4 Conseils pratiques pour être GC-friendly

1. Les objets short-lived sont gratuits (ou presque)

Étant donné la structure générationnelle du heap V8, les objets low-living sont en réalité assez cheap d'un point de vue GC, puisque nous payons principalement pour les objets qui survivent.

// Bon : objets temporaires dans une boucle
function process() {
  for (let i = 0; i < 1000; i++) {
    const temp = { x: i, y: i * 2 }; // mort à la fin de l'itération
    doSomething(temp);
  }
}

2. Évitez les fuites mémoire

Un nœud DOM est dit "detached" quand il est retiré du DOM tree mais que du JavaScript le référence encore. Les nœuds DOM détachés sont une cause commune de fuites mémoire.

// Fuite mémoire
let cache = [];
function addElement() {
  const div = document.createElement("div");
  document.body.appendChild(div);
  cache.push(div); // div reste en mémoire même après remove
}
document.body.removeChild(div);

// Nettoyez vos références
cache = [];

Exercice pratique

3. Ne gardez pas de références inutiles

// Event listeners non nettoyés
element.addEventListener('click', handleClick);
// si element est retiré du DOM, handleClick le garde vivant

// Nettoyez
element.removeEventListener('click', handleClick);`

Conclusion

Vous me direz, "oui mais tout ton article parle de Javascript, Audrey, aujourd'hui on fait du TypeScript qui s'occupe de typer et de rendre le code optimisé". Bah bof : TypeScript disparait à la compilation, donc le code qui tourne dans V8, ca reste du Javascript pur.

TypeScript "ne rend pas V8 heureux" de lui-même, par contre, il peut nous aider à écrire du code qui rend V8 heureux : en typant les objets, par exemple, on peut garantir que touts les instances de cet objet ont les mêmes propriétés dans le même ordre. On peut aussi éviter l'exemple ci-dessus de l'enfer de la déoptimisation en typant les arguments des fonctions pour éviter de prendre des strings dans des fonctions qui attendent des numbers.

Mais Typescript n'est utile que si bien utilisé. Si vous écrivez:

function add(a: number | string, b: number | string) {
  return a + b;
}

Ca marche très bien en TypeScript, il ne va rien te dire, le code compile sans erreur. Mais ça sera exactement le même scénario qu'observé ci-dessus: le type union va engendrer un comportement polymorphique au niveau de V8. De même, any annule complètement la protection: TypeScript ne se plaint pas (enfin il peut, voire il devrait mais malheureusement ça ne tient qu'à toi), mais V8 se retrouve dans le pire cas possible.

Un bon usage de TypeScript et un code V8-friendly sont les deux faces de la même pièce. Quand tu écris du TypeScript rigoureux (pas de any, pas d'union types larges sur des hot paths, des interfaces précises), tu obtiens presque gratuitement du JavaScript que V8 peut optimiser agressivement. Mais TypeScript seul ne garantit rien.

Au final, peu importe l'outil, c'est l'intention derrière qui compte :).

Ressources

Je n'ai pas pondu cet article de moi-même et, en toute humilité, ce que j'ai dit ici a déjà été dit et écrit par d'autres, d'autres manières, avec différents angles. Voici les principales contributions qui m'ont aidé à comprendre V8 et à écrire cet article et que je vous encourage fortement à allre consulter si vous voulez continuer à creuser le sujet:

📬 Rejoignez la newsletter

Recevez chaque semaine un condensé des dernières tendances, outils et ressources dev directement dans votre boîte mail.

Découvrir une édition de la newsletter en ligne.