Apprendre JavaScript de zéro jusqu'au niveau expert : syntaxe moderne, DOM, asynchrone et event loop, modules, prototypes, métaprogrammation, performance et mémoire.
Le niveau expert en JavaScript ne se mesure pas au nombre de méthodes connues par cœur. Il se mesure à la capacité d'expliquer pourquoi un code est lent, pourquoi la mémoire d'un onglet grimpe à 2 Go, pourquoi une animation saccade. Ce chapitre descend dans le moteur V8, les outils de mesure et les patterns d'architecture qui tiennent sur la durée.
Code source
│ parsing
▼
AST ──► Ignition (interpréteur, bytecode)
│ fonction « chaude » + profil de types
▼
Maglev / TurboFan (compilateurs JIT optimisants)
│ hypothèse de type invalidée
▼
Désoptimisation → retour au bytecodeLe JIT optimise une fonction en pariant sur les types qu'il a observés. Si additionner(a, b) reçoit toujours deux entiers, le code machine généré fait une addition d'entiers. Le jour où elle reçoit une chaîne, le pari est perdu : désoptimisation.
V8 attribue une « forme » interne aux objets selon leurs propriétés et leur ordre d'ajout. Les objets de même forme partagent un code d'accès optimisé.
// Même forme : accès rapides
const a = { x: 1, y: 2 }
const b = { x: 3, y: 4 }
// Formes différentes : l'ordre compte
const c = { y: 1, x: 2 }
// Changement de forme après création : à éviter dans le code chaud
const d = { x: 1 }
d.y = 2
delete d.x // delete fait souvent basculer l'objet en mode « dictionnaire », lentRègles pratiques :
delete sur des objets à forme fixe ; assignez undefined ou utilisez une MapCes micro-optimisations n'ont d'intérêt que dans les boucles chaudes. Ne les appliquez pas partout.
Rien ne remplace la mesure. Les intuitions de performance en JavaScript sont fausses une fois sur deux.
performance.mark('debut')
traitementLourd()
performance.mark('fin')
performance.measure('traitement', 'debut', 'fin')
console.log(performance.getEntriesByName('traitement')[0].duration)Dans le navigateur, l'onglet Performance des DevTools enregistre une trace : appels de fonctions, recalculs de style, layout, paint. Les « long tasks » (plus de 50 ms) y apparaissent en rouge.
Côté Node :
node --cpu-prof app.js # génère un .cpuprofile lisible dans Chrome DevTools
node --inspect app.js # ouvre chrome://inspect pour déboguer et profiler
node --prof app.js && node --prof-process isolate-*.logPour comparer deux implémentations, un vrai outil de benchmark (mitata, tinybench) gère l'échauffement du JIT, ce que ne fait pas un simple console.time.
À 60 images par seconde, le navigateur dispose de 16 ms par frame. Une tâche de 200 ms fige l'interface.
async function traiterParLots(items, fn, tailleLot = 500) {
for (let i = 0; i < items.length; i += tailleLot) {
items.slice(i, i + tailleLot).forEach(fn)
await (globalThis.scheduler?.yield?.() ?? new Promise(r => setTimeout(r, 0))) // rend la main au navigateur
}
}Pour un vrai calcul, un Worker s'exécute sur un autre thread :
self.onmessage = ({ data }) => {
const resultat = data.nombres.filter(estPremier).length
self.postMessage(resultat)
}
function estPremier(n) {
if (n < 2) return false
for (let d = 2; d * d <= n; d++) if (n % d === 0) return false
return true
}const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
function calculer(nombres) {
return new Promise(resolve => {
worker.onmessage = e => resolve(e.data)
worker.postMessage({ nombres })
})
}Les données sont copiées entre threads via l'algorithme de clonage structuré. Pour de gros buffers, on les transfère sans copie :
const buffer = new Float32Array(10_000_000).buffer
worker.postMessage(buffer, [buffer]) // buffer devient inutilisable côté mainCôté Node, l'équivalent est node:worker_threads.
Lire une propriété de layout (offsetHeight, getBoundingClientRect) après avoir modifié le DOM force un recalcul synchrone. Dans une boucle, c'est le layout thrashing :
// Lent : lecture/écriture alternées, un layout par itération
for (const el of elements) {
el.style.height = `${el.offsetWidth / 2}px`
}
// Rapide : toutes les lectures, puis toutes les écritures
const largeurs = elements.map(el => el.offsetWidth)
elements.forEach((el, i) => (el.style.height = `${largeurs[i] / 2}px`))Pour les animations, requestAnimationFrame et les propriétés composées (transform, opacity) qui n'impliquent ni layout ni paint. L'impact sur les Core Web Vitals est détaillé dans performance web et Lighthouse.
Le ramasse-miettes libère ce qui n'est plus atteignable depuis les racines (variables globales, pile d'appels). Une fuite, c'est une référence oubliée qui garde un objet atteignable.
Les quatre fuites les plus fréquentes :
// 1. Écouteur jamais retiré sur un objet qui vit longtemps
window.addEventListener('resize', () => composant.recalculer())
// 2. Timer jamais arrêté
const id = setInterval(() => rafraichir(donnees), 1000)
// 3. Cache sans borne
const cache = new Map()
function get(cle) {
if (!cache.has(cle)) cache.set(cle, calculer(cle)) // grossit indéfiniment
return cache.get(cle)
}
// 4. Éléments DOM détachés encore référencés
const lignes = [...document.querySelectorAll('tr')]
tableau.remove() // lignes garde tous les <tr> en mémoireCorrections : AbortController pour grouper les écouteurs, clearInterval au démontage, cache LRU borné ou WeakMap, et mise à null des références devenues inutiles.
class LRU {
#max
#map = new Map()
constructor(max = 100) {
this.#max = max
}
get(k) {
if (!this.#map.has(k)) return undefined
const v = this.#map.get(k)
this.#map.delete(k)
this.#map.set(k, v) // remis en fin = plus récent
return v
}
set(k, v) {
this.#map.delete(k)
this.#map.set(k, v)
if (this.#map.size > this.#max) this.#map.delete(this.#map.keys().next().value)
}
}Une Map conserve l'ordre d'insertion : la première clé est la moins récemment utilisée.
Pour traquer une fuite : DevTools → Memory → prendre un heap snapshot, effectuer l'action suspecte plusieurs fois, reprendre un snapshot, comparer. Filtrer sur « Detached » révèle les nœuds DOM orphelins.
Traiter un fichier de 5 Go sans le charger en mémoire :
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { createGzip } from 'node:zlib'
await pipeline(
createReadStream('export.csv'),
createGzip(),
createWriteStream('export.csv.gz'),
)pipeline gère la backpressure : si l'écriture est plus lente que la lecture, la lecture ralentit. Les Web Streams (ReadableStream, TransformStream) offrent le même modèle dans le navigateur et dans fetch :
const res = await fetch('/api/logs')
const lecteur = res.body.pipeThrough(new TextDecoderStream()).getReader()
while (true) {
const { value, done } = await lecteur.read()
if (done) break
afficher(value)
}innerHTML, eval ou new Function avec des données externesObject.prototypeconst malveillant = JSON.parse('{"__proto__": {"admin": true}}')
fusionProfonde({}, malveillant)
;({}).admin // true dans une fusion naïveProtection : ignorer les clés __proto__, constructor, prototype, ou créer les objets avec Object.create(null). Pour les API côté serveur, voir sécuriser une API REST.
Séparer le pur de l'impur. La logique métier dans des fonctions pures, testables sans navigateur ni réseau. Les effets (DOM, fetch, stockage) dans une couche fine autour.
Injecter les dépendances. Une fonction qui reçoit son client HTTP en paramètre se teste sans mock global.
export function creerServiceArticles({ http, cache }) {
return {
async lister() {
if (!cache.has('articles')) cache.set('articles', await http.get('/articles'))
return cache.get('articles')
},
}
}Valider aux frontières. Tout ce qui entre (API, formulaire, localStorage) passe par un schéma. À l'intérieur, on fait confiance aux types.
import { z } from 'zod'
const Article = z.object({
id: z.number().int(),
titre: z.string().min(1),
publie: z.coerce.date(),
})
const articles = z.array(Article).parse(await res.json())Des erreurs typées, pas des chaînes. Des classes d'erreur (HttpError, ValidationError) permettent des catch précis.
Ce projet assemble Proxy, closures, délégation d'événements, microtâches et modules. Objectif : un composant compteur + liste qui se re-rend automatiquement quand l'état change.
export function creerApp(racine, { etat, vue, actions }) {
let planifie = false
const proxy = new Proxy(etat, {
set(o, p, v) {
o[p] = v
planifier()
return true
},
})
function planifier() {
if (planifie) return
planifie = true
queueMicrotask(() => { // regroupe plusieurs modifications en un seul rendu
planifie = false
rendre()
})
}
function rendre() {
const focus = document.activeElement?.dataset.cle
racine.replaceChildren(vue(proxy))
if (focus) racine.querySelector(`[data-cle="${focus}"]`)?.focus()
}
racine.addEventListener('click', e => {
const cible = e.target.closest('[data-action]')
if (cible) actions[cible.dataset.action]?.(proxy, cible.dataset)
})
rendre()
return proxy
}
export function h(tag, props = {}, ...enfants) {
const el = document.createElement(tag)
for (const [k, v] of Object.entries(props)) {
if (k.startsWith('data-')) el.setAttribute(k, v)
else el[k] = v
}
el.append(...enfants.flat().filter(e => e != null && e !== false))
return el
}import { creerApp, h } from './mini.js'
const etat = creerApp(document.querySelector('#app'), {
etat: { compteur: 0, historique: [] },
vue: s =>
h('section', {},
h('h2', { textContent: `Compteur : ${s.compteur}` }),
h('button', { 'data-action': 'incr', textContent: '+1' }),
h('button', { 'data-action': 'reset', textContent: 'Reset' }),
h('ul', {}, s.historique.map(v => h('li', { textContent: v }))),
),
actions: {
incr(s) {
s.compteur++
s.historique = [...s.historique, s.compteur].slice(-5)
},
reset(s) {
s.compteur = 0
s.historique = []
},
},
})Deux écritures dans incr ne déclenchent qu'un rendu grâce à queueMicrotask : c'est le batching que font React et Vue. Pistes pour aller plus loin : un algorithme de diff au lieu de replaceChildren, des composants imbriqués, un proxy profond (chapitre 10), puis une version typée en TypeScript.
Un expert JavaScript mesure avant d'optimiser, sait lire un profil de performance et un heap snapshot, et garde la logique métier loin du DOM. Le reste s'apprend en ouvrant le code source des bibliothèques qu'on utilise tous les jours : avec ce cours, vous avez les clés pour le lire.