iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
JavaScript · Débutant

JavaScript

Apprendre JavaScript de zéro jusqu'au niveau expert : syntaxe moderne, DOM, asynchrone et event loop, modules, prototypes, métaprogrammation, performance et mémoire.

JavaScript ES2025Node.jsDOMVitestVite
01Introduction et environnement02Variables, types et coercition03Conditions et boucles04Fonctions, portée et closures05Objets, tableaux, Map et Set06Le DOM et les événements07Asynchrone : callbacks, promesses, async/await et event loop08Modules, npm, outillage et tests09Prototypes, classes et this10Itérateurs, générateurs, Symbol, Proxy et métaprogrammation11Niveau expert : moteur, performance, mémoire et architecture
Chapitre 11·60 min

Niveau expert : moteur, performance, mémoire et architecture

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.

Comment V8 exécute votre code

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 bytecode

Le 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.

Hidden classes (shapes)

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 », lent

Règles pratiques :

  • initialisez toutes les propriétés dans le constructeur, dans le même ordre
  • n'utilisez pas delete sur des objets à forme fixe ; assignez undefined ou utilisez une Map
  • gardez les fonctions monomorphes : mêmes types d'arguments à chaque appel

Ces micro-optimisations n'ont d'intérêt que dans les boucles chaudes. Ne les appliquez pas partout.

Mesurer

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-*.log

Pour 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.

Ne pas bloquer le thread principal

À 60 images par seconde, le navigateur dispose de 16 ms par frame. Une tâche de 200 ms fige l'interface.

Découper le travail

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
  }
}

Web Workers

Pour un vrai calcul, un Worker s'exécute sur un autre thread :

worker.js
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
}
main.js
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é main

Côté Node, l'équivalent est node:worker_threads.

Rendu et layout

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.

Mémoire

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émoire

Corrections : 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.

Streams

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)
}

Sécurité côté JavaScript

  • jamais d'innerHTML, eval ou new Function avec des données externes
  • une Content Security Policy stricte bloque les scripts injectés
  • la pollution de prototype : fusionner récursivement un JSON utilisateur peut écrire dans Object.prototype
const malveillant = JSON.parse('{"__proto__": {"admin": true}}')
fusionProfonde({}, malveillant)
;({}).admin   // true dans une fusion naïve

Protection : 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.

Architecture : quelques principes qui tiennent

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.

Projet final : un mini-framework réactif

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.

mini.js
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
}
main.js
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.

Précédent
Itérateurs, générateurs, Symbol, Proxy et métaprogrammation

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main