Module 1 · Fondations du web

Le navigateur : DOM, événements, rendu

Tu développes tous les jours pour une plateforme que tu connais bien : le téléphone, ses vues natives, son moteur Hermes qui exécute ton JavaScript. Le web a exactement la même architecture — un « OS de l'application » qui s'appelle le navigateur — mais avec ses propres pièces et son propre vocabulaire. Ce module pose les trois fondations de tout le reste du guide : le DOM (l'arbre qui représente ta page), les événements (comment la page réagit à l'utilisateur) et le rendu (comment le navigateur transforme du texte en pixels). Quand ces trois idées sont claires, React, Next.js et les questions d'entrevue deviennent des variations sur un thème que tu connais déjà.

Le navigateur, ta nouvelle plateforme

Un navigateur — Chrome, Firefox, Safari — n'est pas « une app qui affiche des pages » : c'est une plateforme d'exécution complète, au même titre qu'iOS ou Android. Il télécharge du code (HTML, CSS, JavaScript), l'exécute dans un environnement isolé et sécurisé (le sandbox : une page web ne peut pas lire tes fichiers ni espionner les autres onglets), fournit des services (réseau, stockage, audio, caméra…) et dessine le résultat à l'écran. Ton « appareil cible » n'est plus un iPhone ou un Pixel : c'est le navigateur, quel que soit l'appareil en dessous.

Sous le capot, deux gros moteurs collaborent. Le moteur de rendu lit le HTML et le CSS et fabrique les pixels. Le moteur JavaScript exécute ton code — celui de Chrome s'appelle V8, et c'est aussi celui de Node.js, donc tu le recroiseras souvent. Chaque navigateur a sa propre paire, sous d'autres noms ; l'idée seule compte : un moteur qui dessine, un moteur qui exécute ton JS, et une machinerie qui les fait dialoguer.

Ce dialogue, tu le connais déjà — sous un autre nom. En React Native, ton JavaScript tourne dans Hermes et, pour afficher quelque chose, il ne dessine pas lui-même : il demande au système, à travers un pont, de créer et modifier des vues natives. Dans le navigateur, pareil : ton JS tourne dans V8 et passe par une interface — le DOM, deux sections plus bas — pour demander au moteur de rendu de mettre l'écran à jour. Le JS ne dessine jamais lui-même ; il pilote une machinerie d'affichage qui vit à côté de lui.

💡 Le concept : ton monde et le web, face à face

Fais l'alignement une bonne fois pour toutes. OS mobile + vues nativesnavigateur + moteur de rendu. HermesV8. Le bridge / JSI qui commande les vues nativesle DOM qui commande la page. Tu changes de plateforme, pas de modèle mental.

Une différence culturelle, quand même : sur mobile, tu compiles, tu soumets à un store, l'utilisateur installe. Sur le web, l'application est téléchargée à chaque visite (avec du cache, certes) et exécutée immédiatement, sans installation ni validation. C'est ce qui rend le web si rapide à déployer — tu pousses, c'est en ligne — et aussi pourquoi la performance de chargement y est une obsession : chaque kilooctet est re-téléchargé par quelqu'un, sur un réseau moyen. C'est une des raisons d'être des frameworks modernes (module sur la carte de Next.js).

Du texte aux pixels : le pipeline de rendu

Question d'entrevue classique, et excellente colonne vertébrale pour comprendre le navigateur : « Qu'est-ce qui se passe entre le moment où le HTML arrive et le moment où la page s'affiche ? ». Le serveur envoie du texte (un fichier HTML, des fichiers CSS) ; l'écran affiche des pixels. Entre les deux, le moteur de rendu déroule une chaîne de fabrication précise, le pipeline de rendu. Le tableau la donne en entier, avec ce que chaque étape fabrique et ce qui la fait rejouer — car après le premier affichage, le navigateur ne refait jamais tout : il rejoue seulement les étapes que ton changement a invalidées.

La chaîne, et le raccourci qui la court-circuite
  1. Parser le HTML→ le DOM
  2. Parser le CSS→ le CSSOM
  3. Arbre de renduDOM et CSSOM croisés
  4. Layoutla géométrie : position et taille un reflow repart d'ici
  5. Paintles pixels de chaque couche un repaint repart d'ici
  6. Compositeles couches superposées, à l'écran
transform et opacity
Après le premier affichage, un changement ne rejoue que ce qu'il a invalidé, et l'étape où il rentre décide de son coût. Changer une taille ou une marge rentre au layout : tout ce qui suit est à refaire, c'est le reflow, le plus cher. Changer une couleur rentre au paint : la géométrie est déjà bonne. Et transform ou opacity — la glissière de droite — descendent directement au composite, sans layout ni paint : c'est exactement pour ça qu'on anime celles-là et pas width.
ÉtapeCe qu'elle fabriqueRejouée par quel changement
Parser le HTML Le DOM (Document Object Model) : un arbre où chaque balise devient un nœud<body> parent de <main>, parent d'un <h1> Ajouter, retirer ou déplacer un nœud (append, remove).
Parser le CSS Le CSSOM : les feuilles de style transformées en arbre de règles, organisé pour répondre vite à « quels styles s'appliquent à ce nœud ? ». Charger ou modifier une feuille de style.
Arbre de rendu Le render tree : DOM + CSSOM croisés, avec les styles finaux de chaque nœud visible (un display: none n'y figure même pas). Tout changement de DOM ou de style qui touche la visibilité.
Layout (mise en page) La géométrie : position et taille de chaque élément. Calcul en cascade — la taille d'un parent dépend de ses enfants, et inversement. Taille, marge, police, insertion d'un élément… → c'est le reflow (ou relayout), l'opération la plus coûteuse, surtout en boucle.
Paint (peinture) Les pixels de chaque couche : fonds, bordures, texte, ombres, images. Couleur, ombre, bordure — la géométrie ne bouge pas : c'est un simple repaint, bon marché.
Composite (assemblage) L'image finale, en superposant les couches (comme les calques d'un logiciel de dessin), souvent sur la carte graphique. transform et opacity : ils sautent directement ici, sans layout ni paint — d'où leur fluidité.
🧭 Bon à savoir : tu n'optimiseras pas ça à la main

Personne ne compte les reflows au quotidien — React et le navigateur s'en sortent seuls. Ce vocabulaire sert à comprendre : pourquoi modifier le DOM en rafale peut saccader, et ce que montrent les graphiques de performance des DevTools. Tu as déjà le réflexe en React Native quand tu préfères animer transform plutôt que width avec Reanimated : même pipeline mental, autre plateforme.

✍️ Exercice de lecture

Deux modifications faites en JavaScript sur la page de RendezVous. Pour chacune, demande-toi : reflow ou simple repaint ?

// (A) La carte du service choisi devient verte :
carte.style.backgroundColor = 'seagreen';

// (B) On insère un message de confirmation au-dessus du formulaire :
formulaire.before(messageConfirmation);
Voir le corrigé

(A) Repaint. Changer une couleur de fond ne modifie ni la taille ni la position de quoi que ce soit : la géométrie calculée au layout reste valable, le navigateur se contente de repeindre la zone. Peu coûteux.

(B) Reflow. Insérer un élément au-dessus du formulaire pousse le formulaire (et tout ce qui suit) vers le bas : toutes ces positions doivent être recalculées. Le navigateur refait le layout de la zone concernée, puis repeint, puis recompose. C'est exactement le genre d'opération que React regroupe intelligemment pour toi — on y arrive à la section « Pourquoi React existe ».

Le DOM est un arbre vivant

La pièce maîtresse, maintenant. La confusion la plus répandue — et une question piège d'entrevue — consiste à croire que « le DOM » est un synonyme chic de « le HTML ». Non. Le HTML est un fichier texte figé, celui que le serveur t'a envoyé. Le DOM est la structure en mémoire que le navigateur construit en lisant ce fichier, et cette structure est vivante : ton JavaScript la lit, la modifie, y greffe ou en coupe des branches, et le navigateur répercute à l'écran. Deux minutes après le chargement, le DOM d'une page interactive ne ressemble souvent plus du tout au HTML de départ.

Sa forme est celle d'un arbre généalogique : tout en haut le nœud racine document, puis <html>, <head> et <body>, puis toute ta page. Chaque balise est un nœud (node) avec un parent, éventuellement des enfants et des frères (siblings, qui partagent le même parent) ; même le texte à l'intérieur d'une balise est un nœud. Ce vocabulaire d'arbre revient sans arrêt : en CSS (les sélecteurs descendants), dans la propagation des événements (section suivante), et dans React (l'arbre de composants).

L'arbre, et un nœud qui n'y est pas encore
documentla racine
<html>
<head>deux frères : même parent
<body>
<main>
<p id="resume">le parent des deux nœuds ci-dessous
« Massothérapie »le texte aussi est un nœud
<span class="badge">créé, pas encore branché
Le vocabulaire au complet, sur un seul dessin : document est la racine ; <head> et <body> sont des frères, puisqu'ils partagent le même parent ; le texte « Massothérapie » est un nœud à part entière, pas un attribut de son parent. Le <span class="badge"> en pointillé est le point qui surprend : createElement l'a fabriqué, il existe en mémoire, mais tant qu'un append ne l'a pas branché dans l'arbre, il n'apparaît nulle part.
🔗 Pont — Le DOM et l'arbre de vues de React Native

Tu manipules déjà un arbre de ce genre tous les jours : en React Native, ton écran est un arbre de vues natives (une View qui contient des View qui contiennent des Text). Le DOM est l'équivalent web — un arbre de nœuds (div, form, p…) que la plateforme sait dessiner. D'où le fait que React marche dans les deux mondes avec le même modèle : il calcule quoi changer dans un arbre, puis un « traducteur » applique les changements — react-dom côté web, React Native côté mobile. Détaillé dans le module « React web ↔ React Native : les ponts ».

Quatre gestes couvrent l'essentiel de la conversation entre ton JavaScript et cet arbre : trouver un nœud, lire ou écrire son contenu, jouer avec ses classes CSS, et créer puis brancher de nouveaux nœuds. L'exemple vient de RendezVous : afficher un résumé du service choisi.

rendezvous/resume-service.js
// La page contient : <select id="service">…</select>
// et un paragraphe vide : <p id="resume"></p>

// 1. TROUVER un nœud : querySelector accepte n'importe quel
//    sélecteur CSS ('#id', '.classe', 'form button'…) et retourne
//    le PREMIER nœud de l'arbre qui correspond.
const select = document.querySelector('#service');
const resume = document.querySelector('#resume');

// 2. LIRE / ÉCRIRE son contenu texte.
resume.textContent = 'Aucun service choisi pour le moment';

// 3. Jouer avec ses CLASSES CSS (le style reste dans le CSS,
//    le JS ne fait qu'activer/désactiver des étiquettes).
resume.classList.add('resume--vide');     // ajoute la classe
resume.classList.remove('resume--vide');  // la retire
resume.classList.toggle('resume--vide');  // bascule

// 4. CRÉER un nœud et le BRANCHER dans l'arbre.
const badge = document.createElement('span'); // orphelin, en mémoire
badge.textContent = 'Populaire';
badge.classList.add('badge');
resume.append(badge);   // branché comme enfant → visible à l'écran

À l'étape 3, les trois lignes forment un catalogue, pas une séquence : dans du vrai code on n'appelle qu'une de ces méthodes à la fois. Et note l'étape 4 : tant que badge n'est pas branché dans l'arbre avec append, il existe en mémoire mais n'apparaît nulle part — créer un nœud et l'afficher sont deux gestes distincts.

📅 Dans RendezVous

Ce trio — un <select> de services, un résumé mis à jour, des classes CSS qu'on bascule — est le squelette de la page qui nous suivra tout le guide. Ici on le fait « à la main », en vanilla JS (du JavaScript nu, sans bibliothèque) : quand on reconstruira la même page en React puis en Next.js, tu verras précisément ce que le framework fait à ta place.

🧠 Quiz éclair

Tu affiches « Afficher le code source » sur la page RendezVous, puis, quelques secondes plus tard — après avoir choisi un service — l'onglet Elements des DevTools. Voici les deux vues du même paragraphe. Laquelle des deux ment ?

<!-- « Afficher le code source » -->
<p id="resume"></p>

<!-- Onglet Elements, quelques secondes plus tard -->
<p id="resume" class="resume--rempli">
  Vous avez choisi : Massothérapie
  <span class="badge">Populaire</span>
</p>

Aucune des deux : elles ne montrent pas la même chose. « Afficher le code source » montre le HTML — le fichier texte que le serveur a envoyé, figé une fois pour toutes. L'onglet Elements montre le DOM — l'arbre en mémoire, tel qu'il est maintenant, après que ton JavaScript l'a modifié (textContent, classList.add, append). Le second est l'état courant, le premier l'état de départ, et l'écart entre les deux mesure exactement ce que le JS a fait. C'est aussi la raison pour laquelle une page dont le contenu est fabriqué en JavaScript apparaît vide dans le code source — un détail qui deviendra central au module « Server et Client Components ».

Les événements : la page te parle

Un DOM qu'on peut modifier, c'est bien ; encore faut-il savoir quand le modifier. C'est le rôle des événements. Le navigateur observe tout — un clic, une touche enfoncée, un champ modifié, un formulaire soumis, la page qui finit de charger — et pour chaque fait notable, il émet un événement sur le nœud concerné. Tu t'abonnes à ceux qui t'intéressent en fournissant une fonction — un callback, au sens du module sur le JavaScript asynchrone du guide Halterofit — et le navigateur l'appellera à chaque occurrence. C'est le grand patron du web : écouter, puis réagir.

L'outil d'abonnement s'appelle addEventListener et se lit comme une phrase : « sur ce nœud, quand tel type d'événement survient, exécute cette fonction ». Quand le navigateur appelle ta fonction, il lui passe l'objet event, un dossier complet sur ce qui vient de se passer — type d'événement, élément concerné, touche pressée, coordonnées du clic…

rendezvous/choix-service.js
const select = document.querySelector('#service');
const resume = document.querySelector('#resume');

// « Sur select, quand l'événement 'change' survient, exécute ceci. »
select.addEventListener('change', (event) => {
  // event.target = le nœud qui a émis l'événement (ici le select).
  // Pour un select, .value est l'option actuellement choisie.
  const service = event.target.value;
  resume.textContent = 'Vous avez choisi : ' + service;
});

Personne n'« appelle » cette fonction dans ton code : tu la déposes, et c'est le navigateur qui l'exécutera, plus tard, à chaque changement du menu. On dit que le code est piloté par les événements (event-driven).

Deux propriétés de l'objet event se ressemblent et leur différence est une question d'entrevue récurrente. target est l'élément sur lequel l'événement s'est réellement produit — l'endroit exact du clic. currentTarget est l'élément qui porte le listener en train de s'exécuter. Quand on clique directement sur l'élément écouté, les deux coïncident. Mais si tu écoutes une carte entière et que le clic tombe sur un bouton à l'intérieur : target est le bouton (le point d'impact), currentTarget la carte (ton poste d'écoute). Pourquoi le listener de la carte se déclenche-t-il alors ? C'est la question de la propagation — section suivante.

✍️ Exercice de lecture

Sur la page RendezVous, chaque service est présenté dans une carte <article class="service"> qui contient un titre et un bouton :

// HTML de la carte :
// <article class="service">
//   <h3>Massothérapie</h3>
//   <button>Choisir</button>
// </article>

const carte = document.querySelector('.service');

carte.addEventListener('click', (event) => {
  console.log('target :', event.target.tagName);
  console.log('currentTarget :', event.currentTarget.tagName);
});

Question : l'utilisateur clique précisément sur le bouton « Choisir ». Qu'affichent les deux console.log ? Et s'il clique sur le fond de la carte, à côté du titre ?

Voir le corrigé

Clic sur le bouton : target : BUTTON et currentTarget : ARTICLE. L'événement s'est produit sur le bouton (c'est le point d'impact, donc le target), mais il est traité par le listener posé sur la carte (currentTarget), que l'événement a atteint en remontant l'arbre.

Clic sur le fond de la carte : target : ARTICLE et currentTarget : ARTICLE — le point d'impact et le poste d'écoute sont le même nœud, les deux propriétés coïncident. Moyen mnémotechnique : target = où ça a frappé, currentTarget = où j'écoute.

🧠 Quiz éclair

Le résumé s'affiche bien une fois, au chargement de la page — puis plus jamais : changer de service ne le met pas à jour. Aucune erreur dans la console. Que s'est-il passé ?

const select = document.querySelector('#service');
const resume = document.querySelector('#resume');

function afficherResume() {
  resume.textContent = 'Vous avez choisi : ' + select.value;
}

select.addEventListener('change', afficherResume());

Les parenthèses de la dernière ligne. afficherResume() appelle la fonction immédiatement — d'où le résumé affiché une fois, au chargement — et c'est sa valeur de retour qui part dans addEventListener. Comme la fonction ne retourne rien, le navigateur reçoit undefined en guise d'écouteur : plus personne n'est abonné à change, et il n'a aucune raison de se plaindre. Il fallait passer la fonction elle-même, sans l'appeler : select.addEventListener('change', afficherResume). La règle générale, et elle vaut aussi pour onClick={...} en React : un callback se dépose, il ne s'appelle pas.

Propagation, délégation, preventDefault

Voici le mécanisme qui explique l'exercice précédent. Quand un événement se produit sur un nœud, il ne reste pas sur place : il voyage dans l'arbre, en deux phases. D'abord la capture : l'événement descend de la racine (document) jusqu'au nœud cible, en traversant tous ses ancêtres. Puis le bouillonnement (bubbling, « la bulle qui remonte ») : il refait le chemin inverse, du nœud cible vers la racine. À chaque étage, dans les deux sens, les listeners des nœuds traversés peuvent se déclencher. En pratique, addEventListener écoute par défaut pendant la remontée — la capture sert à des cas avancés, rares. Retiens l'image simple : un clic sur un bouton « bouillonne » vers ses parents, jusqu'en haut de l'arbre.

Le voyage d'un clic : il descend, puis il remonte
capture
document
<body>
#liste-servicesle listener unique de la délégation
.service
<button>le clic frappe ici
bouillonnement
Le clic frappe le <button>, en rouge — mais l'événement ne reste pas sur place. Il part d'abord du haut et descend jusqu'à lui : c'est la capture, la glissière de gauche. Puis il refait le chemin en sens inverse et remonte vers la racine : c'est le bouillonnement, la glissière de droite. À chaque étage, dans les deux sens, un listener posé sur le nœud traversé peut se déclencher — et comme addEventListener écoute par défaut à la remontée, un seul listener sur #liste-services voit passer les clics de toutes les cartes. C'est toute la délégation.

Ce comportement est un cadeau : il permet la délégation d'événements. Plutôt que de poser un listener sur chacune des vingt cartes de service de RendezVous, tu en poses un seul sur leur conteneur commun, où tous les clics remonteront. Moins de listeners à gérer, et surtout — c'est le vrai argument — les cartes ajoutées plus tard par ton JavaScript sont couvertes automatiquement, puisque c'est le conteneur qui écoute.

✍️ Exercice de lecture

La liste des services de RendezVous, avec un seul listener pour toutes les cartes :

// <section id="liste-services"> contient N cartes
// <article class="service" data-nom="Massothérapie">…</article>

const liste = document.querySelector('#liste-services');

liste.addEventListener('click', (event) => {
  // closest() remonte depuis le point d'impact jusqu'à trouver
  // un ancêtre (ou lui-même) qui correspond au sélecteur.
  const carte = event.target.closest('.service');
  if (!carte) return; // clic entre deux cartes : rien à faire

  afficherResume(carte.dataset.nom); // lit data-nom="…"
});

Questions : (1) Pourquoi un seul listener suffit-il pour toutes les cartes, y compris celles qui n'existent pas encore ? (2) À quoi sert la ligne if (!carte) return; ?

Voir le corrigé

(1) Grâce au bouillonnement : un clic n'importe où dans une carte remonte l'arbre et finit par traverser #liste-services, où le listener l'intercepte. Comme c'est le conteneur qui écoute, peu importe le nombre de cartes ou le moment où elles ont été ajoutées : une carte insérée dans dix minutes par un append sera couverte exactement pareil, sans une ligne de code en plus. C'est ça, la délégation.

(2) Le listener reçoit tous les clics qui traversent la section — y compris un clic sur le titre de la section ou dans l'espace entre deux cartes. Dans ce cas, closest('.service') ne trouve aucune carte ancêtre et retourne null : on sort sans rien faire. C'est le garde-fou standard du patron de délégation.

Dernière pièce : event.preventDefault(). Beaucoup d'éléments HTML ont un comportement par défaut câblé dans le navigateur — un lien navigue vers son adresse, une case se coche… et surtout un formulaire soumis recharge la page entière en envoyant ses données au serveur, héritage du web d'avant JavaScript. preventDefault() dit : « je prends le relais, n'exécute pas ton comportement par défaut ». C'est LE geste fondateur de tout formulaire moderne — on annule le rechargement, puis on traite la soumission en JavaScript (validation, envoi réseau, confirmation) sans quitter la page.

rendezvous/envoi-formulaire.js
const formulaire = document.querySelector('#prise-rdv');

formulaire.addEventListener('submit', (event) => {
  event.preventDefault(); // stoppe le rechargement de la page

  // À partir d'ici, c'est NOUS qui gérons la soumission :
  // valider les champs, envoyer au serveur (module « HTTP & fetch »),
  // afficher la confirmation — sans jamais quitter la page.
});

Sans la première ligne, la page se recharge à la soumission et tout ton état JavaScript est perdu. Ce réflexe est si central qu'on lui consacrera un développement complet dans le module « Formulaires 1 — contrôlé ou non ».

📅 Dans RendezVous

Le formulaire de prise de rendez-vous — nom, service, date, bouton « Confirmer » — est le cœur de l'app-exemple. En vanilla, sa vie tient en trois temps : submit écouté, preventDefault(), envoi au serveur. Réécrit en React (modules Formulaires) puis avec les Server Actions de Next.js (module « Route Handlers & Server Actions »), ces trois temps ne disparaissent jamais : ils changent de responsable.

📖 La formule

Le nom d'événement et son callback

element.addEventListener('click', (event) => {
  event.preventDefault(); // si l'élément a un comportement par défaut
  // ... réagir : lire des valeurs, modifier le DOM, appeler le réseau
});

C'est le patron le plus répété de tout le frontend : tu le verras tel quel en vanilla JS, dans les DevTools, dans les vieux tutoriels — et déguisé en onClick={...} ou onSubmit={...} en React, où c'est la bibliothèque qui pose l'écouteur pour toi (on le re-démontera dans le module « React web ↔ React Native : les ponts »). Chaque fois que tu vois un nom d'événement et un callback, c'est cette formule.

🧠 Quiz éclair

Un listener est posé sur une carte ; l'utilisateur clique sur un bouton à l'intérieur. Que valent event.target et event.currentTarget, et grâce à quel mécanisme le listener se déclenche-t-il ?

target = le bouton (le point d'impact), currentTarget = la carte (le nœud qui porte le listener). Le listener se déclenche grâce au bouillonnement : l'événement remonte du bouton vers ses ancêtres, et la carte le voit passer. C'est ce mécanisme qui rend possible la délégation d'événements.

L'event loop : qui exécute tes callbacks ?

Quand l'utilisateur clique, qui appelle concrètement ta fonction, et quand ? La réponse, tu la connais : la boucle d'événements (event loop) du module sur le JavaScript asynchrone du guide Halterofit (l'asynchrone : promesses et async/await). Rappel express : le JavaScript s'exécute sur un seul fil, avec une file de tâches ; la boucle prend la suivante, l'exécute jusqu'au bout, passe à la suivante. Un clic ne t'interrompt donc jamais en plein calcul : le navigateur dépose ton callback dans la file.

Ce module ajoute la pièce qui manquait : le rendu aussi passe par cette boucle. Entre deux tâches, le navigateur profite des moments libres pour dérouler son pipeline et rafraîchir l'écran — idéalement 60 fois par seconde. Conséquence : si un callback prend 500 ms, la boucle est occupée et rien d'autre ne se passe — pas de rendu, pas de réaction aux clics, pas de défilement : la page est gelée. C'est le « jank » que tu connais en React Native quand un calcul bloque le thread JS. Même cause, même remède : garder les callbacks courts et déléguer le lourd (au serveur, à un Web Worker — un fil JavaScript séparé fourni par le navigateur — ou en découpant le travail).

Un seul fil : ce que le rendu y prend, et ce qu'un callback lui vole
des callbacks courts — l'écran suit
le fil JS tâche tâche tâche tâche
un callback de 500 ms — la page est gelée
le fil JS un seul callback : 500 ms
0 200 ms 400 ms
Une colonne vaut 50 ms, et les deux pistes sont le même fil unique. En vert, les moments où le navigateur a pu rafraîchir l'écran : en haut, il en trouve entre chaque tâche ; en bas, un unique callback occupe le fil pendant 500 ms et il n'y en a aucun — pas de rendu, pas de réaction aux clics, pas de défilement. C'est le « jank ». Les rafraîchissements sont espacés ici pour qu'ils se voient ; en régime normal le navigateur en vise soixante par seconde.
🔗 Pont — La boucle du guide Halterofit, vue côté navigateur

Dans le guide Halterofit, la boucle d'événements servait à comprendre fetch, les promesses et await. Ici tu vois son autre visage : c'est la même boucle qui exécute les callbacks d'événements DOM et qui laisse (ou non) le navigateur rafraîchir l'écran. DOM, événements, asynchrone et rendu ne sont pas quatre sujets : ce sont quatre clients d'un seul fil d'exécution. « Bloquer le thread JS » en React Native et « bloquer le main thread » sur le web, c'est le même bug avec deux accents.

🧠 Quiz éclair

Pourquoi un callback JavaScript qui dure une demi-seconde gèle-t-il la page ?

Parce que le JavaScript tourne sur un seul fil, géré par la boucle d'événements — et que le rendu et les réactions aux événements passent par cette même boucle. Tant que ton callback occupe le fil, rien d'autre ne s'exécute : pas de rafraîchissement d'écran, pas de réponse aux clics. C'est l'équivalent web du thread JS bloqué en React Native.

Pourquoi React existe

Tu sais maintenant tout faire à la main — et c'est précisément ce qui rend visible le problème que React résout. En vanilla, l'état de l'application n'existe nulle part en propre : il est éparpillé dans le DOM, dans le texte des nœuds. Fais grandir RendezVous (le service choisi apparaît dans le résumé, et dans le titre, et dans le prix, et il change les créneaux) et chaque événement doit penser à mettre à jour chaque endroit concerné ; un oubli et l'écran ment — « Massothérapie » dans le résumé, le prix de l'ostéopathie à côté. React répond par la formule que tu connais déjà (module sur le modèle mental de React) : UI = fonction(état). L'état sort du DOM, tu décris l'écran en fonction de lui, et à chaque changement React compare l'ancienne description à la nouvelle — la réconciliation — pour appliquer au DOM les mutations minimales. Toutes les manipulations de ce module (createElement, append, textContent, addEventListener) ont encore lieu ; c'est lui qui les orchestre, sans rien oublier et en groupant les écritures pour limiter les reflows. Rien de neuf pour toi : en React Native, c'était déjà ce mécanisme, avec l'arbre des vues natives à la place du DOM — seule la cible des mutations change (module « React web ↔ React Native : les ponts »).

🔗 Pont — onClick n'est qu'un addEventListener géré pour toi

Quand tu écriras <button onClick={confirmer}> en React web, aucune magie : React pose des écouteurs sur le DOM avec le mécanisme exact de ce module, reçoit les vrais objets event du navigateur et te les repasse à peine emballés. preventDefault(), target, la propagation restent vrais à l'intérieur d'un handler React. Même contrat qu'entre onPress et le système de gestes natif en RN : le framework gère l'abonnement, l'événement vient de la plateforme.

Les DevTools : ton outil n°1

Sur mobile, inspecter ton app demande tout un attirail : Metro, Flipper, les React DevTools branchées sur le device. Sur le web, l'inspection est intégrée au navigateur, gratuite, à un clic droit de distance : les DevTools (clic droit → « Inspecter », ou F12). C'est l'outil numéro un du frontend, au quotidien comme en entrevue où « ouvre les DevTools et montre-moi » est une demande banale. Trois onglets font 90 % du travail :

  • Elements (l'inspecteur) : l'arbre DOM vivant, en direct. Tu survoles un nœud, il se surligne dans la page ; tu modifies son texte, ses classes, son CSS, et tu vois le résultat instantanément. Sur une app React, il montre le DOM que React a produit — utile pour vérifier ce que tes composants génèrent vraiment.
  • Console : tes console.log, les erreurs JavaScript avec leur trace, et un interpréteur où tu dialogues avec la page en direct — document.querySelector(...) y fonctionne tel quel.
  • Network (réseau) : chaque requête émise par la page — HTML initial, CSS, JS, images, appels d'API — avec statut, durée et contenu des réponses. C'est ici que tu débogueras tes fetch (module « HTTP & fetch : parler au réseau »).

Prends l'habitude d'ouvrir les DevTools sur les sites que tu visites et de te promener dans l'onglet Elements. Lire le DOM de vraies pages en production — repérer l'arbre, les classes, ce qui change quand tu interagis — est un exercice de lecture parfait pour tes pauses.

🎤 En entrevue

« Explique-moi ce qui se passe entre le moment où je tape une URL et le moment où la page s'affiche. »

Version courte et structurée : « Le navigateur résout le nom de domaine en adresse IP (DNS), ouvre une connexion au serveur et envoie une requête HTTP. Le serveur répond avec du HTML. Le navigateur le parse en un arbre, le DOM ; il télécharge et parse le CSS en CSSOM ; il combine les deux en arbre de rendu, calcule la géométrie de chaque élément (layout), peint (paint) et assemble les couches (composite). En parallèle, il télécharge et exécute le JavaScript, qui peut modifier le DOM — et chaque modification rejoue la partie nécessaire du pipeline. » La partie réseau est détaillée dans le module « HTTP & fetch » ; en entrevue, l'important est de dérouler les étapes dans l'ordre, sans en inventer.

« Quelle est la différence entre event.target et event.currentTarget ? »

« target est l'élément sur lequel l'événement s'est réellement produit — le point d'impact du clic. currentTarget est l'élément qui porte le listener en cours d'exécution. Ils diffèrent dès que l'événement a bouillonné : si j'écoute un conteneur et qu'on clique sur un bouton à l'intérieur, target est le bouton, currentTarget le conteneur. C'est le fondement de la délégation d'événements : un seul listener sur le parent, et target me dit où le clic a eu lieu. »

« Le DOM et le HTML, c'est la même chose ? »

« Non. Le HTML est le document texte envoyé par le serveur ; le DOM est la structure d'objets que le navigateur construit en le parsant, et qui vit en mémoire. Le JavaScript peut modifier le DOM à tout moment, donc le DOM d'une page interactive diverge vite du HTML initial. L'onglet Elements des DevTools montre le DOM courant ; “Afficher le code source” montre le HTML reçu — comparer les deux sur une app React est très parlant. »

window, document et les autres APIs du navigateur

Survol du paysage, pour savoir ce qui existe. Ton JavaScript vit sous un objet global, window, qui représente l'onglet lui-même. document — le point d'entrée du DOM — est une de ses propriétés, comme la quasi-totalité des services du navigateur. On appelle l'ensemble les Web APIs : des capacités fournies par la plateforme, au même titre que les modules natifs d'Expo donnent accès à la caméra sur mobile. Quelques-unes que tu recroiseras :

  • fetch — parler au réseau ; le module « HTTP & fetch : parler au réseau » la reprend en profondeur côté web.
  • localStorage — stockage clé-valeur qui survit à la fermeture de l'onglet. Le cousin très simplifié de ton MMKV : quelques chaînes, pas une base de données.
  • history et location — l'URL courante et l'historique. C'est là-dessus que les routeurs modernes (dont celui de Next.js, module « Routing : segments, layouts, navigation ») construisent la navigation sans rechargement.
  • setTimeout, requestAnimationFrame — planifier du code plus tard, déjà croisées avec l'event loop.

Un point conceptuel qui vaut d'être dit une fois : ces APIs ne font pas partie du langage JavaScript. Le langage, c'est la norme ECMAScript — variables, fonctions, promesses, tableaux. document, fetch et localStorage sont fournis par la plateforme qui héberge ton code : voilà pourquoi document n'existe ni en React Native ni dans Node.js. Autre plateforme, autres APIs, même langage — distinction qui éclairera une subtilité majeure de Next.js, où du code exécuté sur le serveur n'a pas accès au DOM (module « Server et Client Components »).

⚠️ Piège fréquent

Supposer que document est là. Le symptôme est un ReferenceError: document is not defined — et il ne tombe jamais dans l'onglet, où document existe bel et bien. Il tombe dans un test qui s'exécute sous Node, ou au build d'un composant Next.js rendu sur le serveur.

Le réflexe de diagnostic tient en une question, à se poser devant toute ligne qui touche window ou document : où est-ce que ça s'exécute ? Si la réponse est « ça dépend », la ligne est mal placée. C'est la question centrale du module « Server et Client Components ».

🧭 Bon à savoir

Tu verras souvent window.addEventListener('resize', ...) ou document.addEventListener('keydown', ...) : window et document sont eux-mêmes des cibles d'événements, tout en haut de l'arbre — logique, puisque le bouillonnement y fait remonter tous les événements. La formule « écouter et réagir » s'y applique comme à n'importe quel nœud.

🧠 Quiz éclair

Ce fichier fonctionne parfaitement quand tu le testes dans le navigateur, et fait échouer le build avec ReferenceError: document is not defined. Quelle ligne pose problème, et pourquoi le navigateur, lui, ne se plaint pas ?

const largeurEcran = document.body.clientWidth;   // (1)

export function Bandeau() {
  const compact = largeurEcran < 600;             // (2)
  return compact ? 'Bandeau court' : 'Bandeau long';
}

La ligne (1) — et plus précisément l'endroit où elle est écrite. Posée au niveau du module, elle s'exécute dès que le fichier est chargé, avant et indépendamment de tout appel à Bandeau(). Or au build, c'est un serveur Node qui charge ce fichier pour fabriquer le HTML — et document est une Web API, fournie par l'onglet, jamais par Node. Le navigateur ne se plaint pas parce que, chez lui, document existe bel et bien. Même code, deux lieux d'exécution, deux résultats. Le réflexe : devant toute ligne qui touche window ou document, demande-toi où est-ce que ça tourne — et si la réponse est « ça dépend », déplace la lecture à l'intérieur du composant, là où on peut la conditionner.

À retenir

Le navigateur est ta nouvelle plateforme : un moteur de rendu qui fabrique les pixels, un moteur JS (V8, celui de Node) qui exécute ton code — l'équivalent web du duo « vues natives + Hermes ». Le HTML est parsé en un DOM, arbre vivant que ton JavaScript lit et modifie (querySelector, textContent, classList, createElement/append). Le pipeline DOM → CSSOM → layout → paint → composite transforme le texte en pixels ; changer la géométrie coûte un reflow, changer une couleur un simple repaint. La page réagit par événements : formule « écouter et réagir », bouillonnement (target = impact, currentTarget = poste d'écoute) et délégation. L'event loop exécute tes callbacks — et c'est la même qui laisse (ou non) le navigateur rafraîchir l'écran. React existe parce que tout ça à la main ne passe pas à l'échelle : il orchestre les mutations DOM minimales à ta place.

Et ailleurs : Le trio de ce module — un arbre d'interface, des événements qui s'y propagent, une boucle qui exécute tes réactions — n'a rien de propre au web : les interfaces desktop (Qt, GTK, Windows) reposent dessus depuis les années 80, et même un jeu vidéo boucle sur « lire les entrées, mettre à jour l'état, redessiner ». Apprendre le DOM, c'est apprendre l'instance web d'un patron universel. En entrevue, savoir dire « même modèle arbre + événements qu'en React Native, seule la plateforme change » montre exactement le recul qu'on attend d'un candidat qui vient du mobile.

🗂️ L'aide-mémoire
L'ordre des étapes du pipeline de rendu
DOM, CSSOM, arbre de rendu, layout, paint, composite — le CSSOM se parse en parallèle du DOM, les quatre suivantes sont bien une séquence
Ce qui sépare un reflow d'un repaint
la géométrie. Une taille, une marge, une insertion de nœud rentrent au layout : tout ce qui suit est à refaire. Une couleur rentre au paint
Les deux propriétés qui descendent directement au composite
transform et opacity — ni layout ni paint. C'est pour ça qu'on anime celles-là et pas width
La phase que addEventListener écoute par défaut
la remontée (le bouillonnement), pas la capture. La capture demande un troisième argument, et sert à des cas rares
event.target contre event.currentTarget
target = où le clic a frappé ; currentTarget = le nœud qui porte le listener. Ils ne diffèrent que si l'événement a bouillonné
Ce qui manque à un nœud fabriqué par createElement
un append. Il existe en mémoire et n'apparaît nulle part tant qu'il n'est pas branché dans l'arbre : créer et afficher sont deux gestes
Ce que les parenthèses changent dans addEventListener('change', f())
tout : f() l'appelle immédiatement et abonne sa valeur de retour. Un callback se dépose, il ne s'appelle pas
Le geste qui empêche un formulaire de recharger la page
event.preventDefault(), en première ligne du handler de submit. Sans lui, tout l'état JavaScript est perdu
Ce que montre « Afficher le code source », et que l'onglet Elements ne montre pas
le HTML reçu du serveur, figé. Elements montre le DOM courant ; l'écart entre les deux est exactement ce que le JavaScript a fait
Où vivent document, fetch et localStorage
dans la plateforme, pas dans le langage — ce sont des Web APIs, pas de l'ECMAScript. D'où leur absence sous Node et en React Native