Chapitre 2
Chapitre 02 — Comment fonctionne le navigateur
Chapitre 02 — Comment fonctionne le navigateur
Cours 02 — Comment fonctionne le navigateur
1. Architecture d'un navigateur
1.1 Vue d'ensemble
Un navigateur moderne est une suite de sous-systèmes qui interagissent pour transformer du code (HTML, CSS, JavaScript) en pixels interactifs à l'écran.
Diagramme en cours de génération...
1.2 Les composants
-
User Interface : Barre d'adresse, boutons, menus. C'est ce que l'utilisateur voit et avec quoi il interagit directement (en dehors de la page web elle-même).
-
Browser Engine : Le chef d'orchestre. Il coordonne les actions entre l'UI et le Rendering Engine. Il gère la navigation (historique, onglets), les bookmarks, et les permissions.
-
Rendering Engine : Le cœur du navigateur pour afficher les pages. Il parse le HTML et CSS, construit le DOM et CSSOM, calcule le layout, et paint les pixels.
-
Networking : Gère toutes les communications réseau (HTTP/HTTPS, DNS lookup, TLS handshake). Il implémente des optimisations comme HTTP/2 multiplexing et HTTP/3 QUIC.
-
JavaScript Interpreter : Exécute le code JavaScript. Chaque navigateur a son moteur : V8 (Chrome), SpiderMonkey (Firefox), JavaScriptCore (Safari).
-
Data Storage : Gère la persistance des données : Cookies, LocalStorage, SessionStorage, IndexedDB, Cache API, File System API.
1.3 Architecture multi-processus (Chrome)
Chrome a popularisé l'architecture multi-processus. Chaque onglet est un processus séparé. Cela apporte :
- Isolation : Un onglet qui plante ne tue pas les autres
- Sécurité : Un onglet ne peut pas accéder à la mémoire d'un autre
- Performance : Les processus peuvent être répartis sur différents cœurs CPU
Diagramme en cours de génération...
Inconvénient : Chaque processus a sa propre mémoire. Chrome est connu pour sa consommation mémoire élevée (~50-200MB par onglet).
2. Moteurs de rendu
2.1 Les principaux moteurs
| Navigateur | Moteur de rendu | Moteur JS | Année |
|---|---|---|---|
| Chrome / Edge / Opera / Brave | Blink (fork de WebKit) | V8 | 2013 |
| Safari | WebKit | JavaScriptCore (Nitro) | 2003 |
| Firefox | Gecko | SpiderMonkey | 2004 |
| Samsung Internet | Blink | V8 | 2012 |
Note historique : Blink est un fork de WebKit créé par Google en 2013 pour permettre plus d'innovation sans dépendre du cycle de release d'Apple. Aujourd'hui, Blink et WebKit ont divergé significativement.
2.2 Architecture d'un moteur de rendu
Diagramme en cours de génération...
3. Le Critical Rendering Path (CRP)
3.1 Définition
Le Critical Rendering Path est la séquence d'étapes que le navigateur effectue pour convertir HTML, CSS et JavaScript en pixels à l'écran. Comprendre le CRP est essentiel pour optimiser le First Paint et le Time to Interactive.
3.2 Étape 1 : HTML → DOM (Document Object Model)
<html>
<head>
<title>Page</title>
</head>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>
Le parseur HTML convertit cela en un arbre de nœuds :
Diagramme en cours de génération...
Preload Scanner : Le parseur HTML a un "preload scanner" qui parcourt le balisage en parallèle pour identifier les ressources critiques (CSS, scripts, images) et commence à les télécharger avant même que le parseur principal n'atteigne ces éléments.
3.3 Étape 2 : CSS → CSSOM (CSS Object Model)
h1 { color: red; font-size: 24px; }
p { color: blue; }
Le CSSOM est un arbre, comme le DOM, mais avec des règles de cascade appliquées.
CSS est render-blocking : Le navigateur ne paint PAS tant que tout le CSS n'est pas téléchargé et parsé. Pourquoi ? Sans CSS, le browser peindrait avec des styles par défaut, puis devrait tout repeindre une fois le CSS chargé (Flash of Unstyled Content - FOUC).
3.4 Étape 3 : JavaScript
JavaScript est parser-blocking (par défaut). Quand le parseur HTML rencontre une balise <script> :
- Il arrête la construction du DOM
- Il télécharge le script (si externe)
- Il exécute le script
- Il reprend la construction du DOM
Pourquoi ? JavaScript peut modifier le DOM (document.write()) et le CSSOM. Le parseur doit attendre que le script ait fini pour continuer.
Solutions :
async: Télécharge en parallèle, exécute dès que disponible (ordre non garanti)defer: Télécharge en parallèle, exécute après le parse HTML (ordre garanti)
<script src="analytics.js" async></script>
<script src="app.js" defer></script>
3.5 Étape 4 : Render Tree
Le Render Tree combine le DOM et le CSSOM. Il ne contient que les éléments visibles :
<head>et ses enfants sont exclus (non visibles)- Les éléments avec
display: nonesont exclus - Les pseudo-éléments (
::before,::after) sont inclus
Le Render Tree n'est pas le DOM. C'est une structure simplifiée qui ne contient que ce qui doit être affiché.
3.6 Étape 5 : Layout (Reflow)
Le Layout calcule la position et la taille de chaque élément du Render Tree :
- Éléments en blocs empilés verticalement
- Éléments en ligne disposés horizontalement
- Flexbox, Grid : algorithmes de layout spécifiques
- Positionnement absolu/relatif/fixed
Le Layout est coûteux : Pour chaque élément, le navigateur doit calculer sa géométrie en tenant compte de :
- Son contenu
- Ses marges, padding, border
- Son parent (taille disponible)
- Ses enfants (taille nécessaire)
Déclencheurs de Reflow :
- Ajout/suppression d'éléments DOM
- Changement de taille (fenêtre, police, contenu)
- Modification des dimensions CSS (width, height, margin, padding)
offsetHeight,getBoundingClientRect()(force un reflow synchrone)
// ❌ Mauvais : 3 reflows
element.style.width = '100px';
element.style.height = '50px';
element.style.margin = '10px';
// ✅ Bon : 1 reflow
element.style.cssText = 'width: 100px; height: 50px; margin: 10px;';
// Ou mieux : utiliser une classe CSS
element.classList.add('new-dimensions');
3.7 Étape 6 : Paint
Le Paint remplit les pixels. Pour chaque élément, le navigateur dessine :
- Le fond (background-color, background-image)
- Le texte
- Les bordures
- Les ombres, dégradés
- Les images
Le Paint est décomposé en phases : Pour chaque élément, le navigateur dessine dans l'ordre (painting order) :
- Background
- Borders
- Text
- Outlines
Déclencheurs de Repaint (sans Reflow) :
- Changement de couleur (
color,background-color) - Changement de visibilité (
visibility: hidden) - Changement d'ombre (
box-shadow)
3.8 Étape 7 : Compositing
Le Compositing fusionne les différentes couches (layers) en une image finale.
Pourquoi des couches ? Certains éléments sont promus dans leurs propres couches pour des raisons de performance :
transform,opacitydéclenchent la création d'un nouveau calquewill-change: transformle force<video>,<canvas>,<iframe>ont leurs propres calques
Avantage : Si une couche est animée avec transform ou opacity, seul son calque est repeint, pas toute la page. C'est pourquoi les animations CSS avec transform sont plus performantes que celles avec left/top.
/* ✅ Bon : animation sur un calque séparé */
.animated {
will-change: transform;
transition: transform 0.3s;
}
/* ❌ Mauvais : déclenche un reflow à chaque frame */
.animated {
transition: left 0.3s;
}
4. JavaScript Engine : V8
4.1 Architecture V8
Diagramme en cours de génération...
4.2 Parser → AST
Le parser transforme le code source en AST (Abstract Syntax Tree). V8 a deux parseurs :
- Pre-parser : Parse rapidement, ne crée pas l'AST complet pour les fonctions non immédiatement utilisées (lazy parsing)
- Full parser : Parse complètement quand la fonction est appelée
// Cette fonction est "pre-parsed" (non compilée immédiatement)
function lazyFunction() {
return 'I am lazy';
}
// Cette fonction est "fully parsed" (appelée immédiatement)
lazyFunction();
4.3 Ignition (Interpreter)
Ignition est l'interpréteur de V8. Il convertit l'AST en bytecode et l'exécute immédiatement. Le bytecode est plus compact que l'AST et plus rapide à exécuter.
Avantage de l'interprétation : Démarrage rapide. Pas besoin d'attendre la compilation.
4.4 TurboFan (JIT Compiler)
TurboFan est le compilateur JIT (Just-In-Time) de V8. Pendant l'exécution, V8 collecte des informations de type (profiling). Si une fonction est "chaude" (exécutée souvent), TurboFan la compile en code machine optimisé en utilisant les types observés.
function add(a, b) {
return a + b;
}
// Premières exécutions : interprétées, profiling
add(1, 2); // V8 note : a et b sont des nombres
add(3, 4); // V8 confirme : a et b sont des nombres
add(5, 6); // La fonction devient "chaude"
// TurboFan compile add en code machine optimisé pour des arguments numériques
add(7, 8); // Exécution native, très rapide
// Mais si on appelle avec des types différents :
add('hello', 'world'); // DÉOPTIMISATION !
// V8 revient à l'interpréteur (plus de suppositions de types valides)
Déoptimisation : Si un type inattendu est rencontré, V8 "déoptimise" le code compilé et revient à l'interpréteur. C'est une source majeure de ralentissement.
Bonne pratique : Éviter de mélanger les types dans une fonction :
// ❌ Mauvais : déoptimisation
function process(value) {
return value + 1; // Parfois number, parfois string
}
// ✅ Bon : types cohérents
function processNumber(value: number): number {
return value + 1;
}
4.5 Hidden Classes et Inline Caching
V8 optimise l'accès aux propriétés des objets avec :
- Hidden Classes : Structures internes qui décrivent la "forme" d'un objet
- Inline Caching : Cache l'adresse mémoire d'une propriété après le premier accès
// ❌ Mauvais : change la "forme" de l'objet
function Point(x, y) {
this.x = x;
this.y = y;
}
const p1 = new Point(1, 2); // Hidden Class: {x, y}
// Ajouter une propriété après création = nouvelle hidden class
p1.z = 3; // Transition de hidden class → plus lent
// ✅ Bon : toutes les propriétés dans le constructeur
function PointOptimized(x, y, z = 0) {
this.x = x;
this.y = y;
this.z = z; // Défini immédiatement
}
5. Event Loop
5.1 Le modèle d'exécution JavaScript
JavaScript est single-threaded (un seul thread d'exécution) mais non-bloquant (asynchrone). L'Event Loop est le mécanisme qui permet cette illusion de concurrence.
Diagramme en cours de génération...
5.2 Call Stack
La Call Stack est une pile LIFO (Last In, First Out) qui contient les frames des fonctions en cours d'exécution.
function multiply(a, b) { return a * b; }
function square(n) { return multiply(n, n); }
function logSquare(n) { console.log(square(n)); }
logSquare(5);
// Call Stack (au moment de multiply) :
// 1. multiply
// 2. square
// 3. logSquare
// 4. (main)
Stack Overflow : Si la pile dépasse sa capacité (récursion infinie) :
function infinite() { return infinite(); }
infinite(); // RangeError: Maximum call stack size exceeded
5.3 Web APIs
Les opérations asynchrones (setTimeout, fetch, DOM events) sont gérées par les Web APIs (fournies par le navigateur, pas par JS lui-même). Quand une opération asynchrone est terminée, sa callback est placée dans la file appropriée.
5.4 Microtask Queue vs Callback Queue (Macrotask Queue)
Microtask Queue (file prioritaire) :
- Promise.then/catch/finally
- MutationObserver
- queueMicrotask()
- Processée après chaque opération de la Call Stack
Callback Queue (macrotask queue) :
- setTimeout / setInterval
- DOM events (click, scroll)
- I/O
- Processée après que la Microtask Queue est vide
console.log('1: Start');
setTimeout(() => console.log('2: Timeout'), 0);
Promise.resolve().then(() => console.log('3: Promise'));
console.log('4: End');
// Output : 1, 4, 3, 2
// Pourquoi ? La Promise va dans Microtask Queue (prioritaire)
// setTimeout va dans Callback Queue (non prioritaire)
Ordre d'exécution :
- Exécute tout le code synchrone (Call Stack)
- Vide la Microtask Queue (toutes les Promises)
- Prend UNE macrotask de la Callback Queue
- Retour à l'étape 2
5.5 Starvation
function loopForever() {
Promise.resolve().then(loopForever);
}
loopForever();
setTimeout(() => console.log('Never called'), 1000);
Ce code ne libère jamais la Callback Queue parce que la Microtask Queue est constamment remplie. C'est la starvation des macrotasks.
6. Repaint vs Reflow vs Compositing
6.1 Comparaison des coûts
| Opération | Coût | Déclencheurs |
|---|---|---|
| Repaint | Moyen | color, visibility, background-color, box-shadow |
| Reflow | Très coûteux | width, height, margin, padding, font-size, ajout/suppression DOM |
| Compositing seul | Faible | transform, opacity, will-change |
6.2 Forcer un Reflow (layout thrashing)
Certaines propriétés JS forcent un reflow synchrone pour retourner une valeur précise. Les utiliser en boucle est désastreux.
const elements = document.querySelectorAll('.box');
// ❌ Layout Thrashing : à chaque itération, le navigateur recalcule le layout
elements.forEach(el => {
console.log(el.offsetHeight); // force reflow
el.style.height = `${el.offsetHeight + 10}px`; // force reflow
});
// ✅ Batch reading then writing
const heights = [];
elements.forEach(el => {
heights.push(el.offsetHeight); // tous les reads d'abord
});
elements.forEach((el, i) => {
el.style.height = `${heights[i] + 10}px`; // tous les writes ensuite
});
7. RAF, RIC, Web Workers, Service Workers
7.1 requestAnimationFrame (RAF)
requestAnimationFrame exécute une fonction avant le prochain repaint. C'est le standard pour les animations.
// ❌ Mauvaise animation (variable, pas de synchronisation affichage)
setInterval(() => updatePosition(), 16); // ~60fps
// ✅ Bonne animation (synchro avec le refresh rate du moniteur)
function animate() {
updatePosition();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
RAF garantit :
- Pas d'exécution superflue (si l'onglet est caché, RAF ne s'exécute pas)
- Synchronisation avec le refresh rate (60Hz, 120Hz, 144Hz)
- Regroupement avant le repaint
7.2 requestIdleCallback (RIC)
requestIdleCallback exécute une fonction pendant les périodes d'inactivité du navigateur.
// Tâche non critique, exécutée quand le navigateur a du temps libre
requestIdleCallback(() => {
precomputeData();
sendAnalyticsBeacon();
}, { timeout: 5000 });
7.3 Web Workers
Les Web Workers permettent d'exécuter du JavaScript dans un thread séparé. Ils n'ont PAS accès au DOM.
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'compute', data: complexArray });
worker.onmessage = (event) => {
console.log('Result:', event.data);
};
// worker.js
self.onmessage = (event) => {
const result = expensiveCalculation(event.data);
self.postMessage(result);
};
Limitations :
- Pas d'accès au DOM
- Pas d'accès à window, document, parent
- Communication via messages (copie, pas de référence partagée)
- Transferable Objects pour les gros volumes (ArrayBuffer)
Cas d'usage :
- Calculs lourds (cryptographie, compression)
- Traitement d'images/vidéo
- Parsing de gros fichiers
- Websocket handling
7.4 Service Workers
Les Service Workers sont des proxy programmables entre le navigateur et le réseau. Ils interceptent les requêtes réseau et peuvent servir des réponses depuis un cache.
// sw.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll(['/', '/app.js', '/style.css']);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
Cycle de vie :
- Install (première visite)
- Waiting (en attente si ancien SW actif)
- Activate (ancien cache nettoyé)
- Fetch (interception des requêtes)
Cas d'usage :
- Offline support (PWA)
- Cache stratégies (Cache First, Network First, Stale While Revalidate)
- Background sync
- Push notifications
- Préchargement de pages
8. Storage : APIs de persistance
8.1 Comparaison
| API | Taille max | Type | Sync/Async | Durée | Accès SW |
|---|---|---|---|---|---|
| Cookies | 4KB | String | Sync | Expiration définie | Oui |
| localStorage | 5-10MB | String | Sync | Permanent | Non |
| sessionStorage | 5-10MB | String | Sync | Session (onglet fermé) | Non |
| IndexedDB | ~GB (espace disque) | Objets (structured clone) | Async | Permanent | Oui |
| Cache API | ~GB | HTTP Request/Response | Async | Permanent | Oui |
8.2 Cookies
// Définir un cookie
document.cookie = 'sessionId=abc123; Secure; HttpOnly; SameSite=Lax; Max-Age=3600';
// Lire tous les cookies
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [key, value] = cookie.trim().split('=');
acc[key] = value;
return acc;
}, {});
Problèmes : Envoyés dans chaque requête HTTP (même pour les images), limités à 4KB.
Sécurité :
HttpOnly: Inaccessible depuis JavaScript (protection XSS)Secure: Uniquement en HTTPSSameSite: Protection CSRF (Strict, Lax, None)
8.3 IndexedDB
// Ouvrir/ créer une base
const request = indexedDB.open('ProjectHub', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore('tasks', { keyPath: 'id' });
store.createIndex('status', 'status', { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction('tasks', 'readwrite');
const store = transaction.objectStore('tasks');
store.add({ id: '1', title: 'Review PR', status: 'pending' });
};
9. Networking : HTTP/1.1 → HTTP/2 → HTTP/3
9.1 HTTP/1.1 (1999)
Problèmes :
- Head of Line blocking : Une seule requête à la fois par connexion TCP, les suivantes attendent
- Hôte de connexions : Les navigateurs ouvrent 6 connexions parallèles par domaine pour contourner le HoL blocking
- Overhead : En-têtes HTTP non compressés, répétés à chaque requête
Diagramme en cours de génération...
9.2 HTTP/2 (2015)
Améliorations :
- Multiplexing : Plusieurs requêtes simultanées sur UNE connexion TCP
- Header compression (HPACK) : Compression des en-têtes
- Server Push : Le serveur envoie des ressources sans que le client les demande
- Binary protocol : Plus efficace que textuel
Diagramme en cours de génération...
Limitation HTTP/2 : Head of Line blocking au niveau TCP. Si un paquet TCP est perdu, TOUTES les streams HTTP/2 sont bloquées jusqu'à la retransmission.
9.3 HTTP/3 (2022)
Solution : Remplacer TCP par QUIC (Quick UDP Internet Connections), qui utilise UDP.
Avantages HTTP/3 :
- 0-RTT handshake (vs 2-3 RTT pour TCP + TLS)
- Indépendance des streams : La perte d'un paquet n'affecte que son stream, pas les autres
- Connection migration : Changement de réseau (WiFi → 4G) sans reconnexion
Diagramme en cours de génération...
10. Synthèse : Le parcours complet d'une requête
Diagramme en cours de génération...