Skip to content

Pipeline de rendu du navigateur

🎯 Question centrale

Pourquoi certaines pages web sont-elles fluides comme de la soie, tandis que d'autres rament comme un diaporama ? Comment le navigateur transforme-t-il un tas de code HTML, CSS et JavaScript en la page web que vous voyez ? Ce chapitre vous emmène dans les coulisses du navigateur pour comprendre son fonctionnement et ainsi écrire des pages web plus performantes.

Que va vous apprendre cet article ?

ChapitreContenuCe que vous saurez faire après
Chapitre 1Pourquoi comprendre le pipeline de renduComprendre la nécessité de l'optimisation des performances
Chapitre 2Les cinq étapes du pipeline de renduMaîtriser le flux de rendu de base du navigateur
Chapitre 3Construction de l'arbre DOM et du CSSOMComprendre comment le HTML et le CSS sont analysés
Chapitre 4Construction de l'arbre de renduSavoir quels éléments seront rendus
Chapitre 5Layout et ReflowÉviter de déclencher des calculs de mise en page coûteux
Chapitre 6Paint et RepaintRéduire les opérations de peinture inutiles
Chapitre 7Composition et accélération GPUExploiter le GPU pour améliorer les performances d'animation
Chapitre 8Boucle d'événementsComprendre le mécanisme d'exécution de JavaScript
Chapitre 9Optimisation des performances en pratiqueMaîtriser les techniques courantes d'optimisation

Chaque chapitre commence par « comprendre le principe » — pas besoin de savoir écrire du code d'optimisation à la main. Revenez consulter quand vous rencontrez des problèmes de performance.


1. Pourquoi comprendre le « pipeline de rendu » ?

1.1 De « ça marche » à « ça marche vite » : la progression du développeur frontend

Quand on débute en frontend, on se demande seulement si le code « fonctionne » — la page s'affiche, le bouton est cliquable, c'est réussi. Mais à mesure que le projet grandit et que les utilisateurs se multiplient, vous découvrez vite une réalité cruelle : pour la même fonctionnalité, certains développeurs produisent une page d'une fluidité parfaite, tandis que d'autres créent une page tellement saccadée que les utilisateurs veulent jeter leur souris.

C'est comme apprendre à conduire. Le débutant se demande seulement si « la voiture peut avancer », mais le conducteur expérimenté se demande « quand changer de vitesse, quand freiner, comment conduire de façon économique ». Le navigateur est votre « voiture » — comprenez son « comportement » pour rouler vite et en douceur.

🐢 Mentalité débutant (centré sur la fonctionnalité)

  • Tant que la page s'affiche, c'est bon
  • Les saccades, c'est la faute du navigateur
  • L'optimisation, c'est pour plus tard

🚀 Mentalité avancé (centré sur l'expérience)

  • La fluidité est au cœur de l'expérience utilisateur
  • Comprendre le fonctionnement interne du navigateur
  • Penser aux performances dès l'écriture du code

Comprendre le pipeline de rendu, c'est l'étape clé pour passer de « ça marche » à « ça marche vite ».

1.2 Une histoire vraie : pourquoi l'« optimisation » a rendu la page encore plus lente ?

La mésaventure de performance de Xiao Zhang

Xiao Zhang est développeur frontend dans une entreprise d'e-commerce. Il doit optimiser la page de détail produit. Cette page rame tellement à l'affichage des informations produit que les utilisateurs se plaignent sans arrêt.

Xiao Zhang se dit : « La page rame parce qu'il y a trop d'éléments DOM. Je vais d'abord cacher les éléments avec display:none, les modifier, puis les réafficher — comme ça le navigateur ne refera pas le rendu à chaque fois, non ? »

Il écrit alors ce code :

javascript
// Ce que vous croyez être une « optimisation »
const container = document.getElementById('list')
container.style.display = 'none'  // Je cache d'abord, ça ne devrait pas déclencher de rendu ?

for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'  // Largeur aléatoire
  container.appendChild(item)
}

container.style.display = 'block'  // J'affiche à la fin, un seul rendu

Résultat : après test, la page est encore plus lente ! Xiao Zhang est abasourdi : il a pourtant « optimisé », pourquoi est-ce pire ?

Plus tard, le lead frontend regarde le code et pointe le problème : même si les éléments sont cachés, chaque modification de style.width déclenche quand même le calcul de style et le marquage de layout ; le navigateur fait un travail énorme et inutile en arrière-plan.

La bonne approche est d'utiliser DocumentFragment pour tout faire en mémoire, puis d'insérer en une seule fois dans le DOM — un seul déclenchement de rendu.

💡 Leçon clé

Sans comprendre le fonctionnement du navigateur, vous risquez d'écrire du code « optimisé » qui, en réalité, dégrade les performances. Comprendre le pipeline de rendu vous permet de savoir quelles opérations sont coûteuses et lesquelles sont bon marché, pour éviter de faire des efforts au mauvais endroit.


2. Concept fondamental : qu'est-ce que le « pipeline de rendu » ?

🤔 Qu'est-ce que le « rendu » ?

Le rendu (Rendering) , c'est tout simplement le processus par lequel le navigateur « dessine » le code pour en faire la page web que vous voyez.

Imaginez une imprimerie qui fabrique un livre :

  • HTML = le contenu du manuscrit (texte, images, chapitres)
  • CSS = les exigences de mise en page (taille de police, couleurs, espacement)
  • JavaScript = les modifications dynamiques (l'auteur modifie le manuscrit, ajuste la mise en page)

Le navigateur reçoit ces « matériaux », les passe par une série d'« étapes », et enfin « imprime » la page web que vous voyez. Cette série d'étapes, c'est le pipeline de rendu (Rendering Pipeline) .

Pour vous aider à mieux comprendre, utilisons une boulangerie comme métaphore du processus de rendu du navigateur.

2.1 Comprendre le pipeline de rendu avec la métaphore de la boulangerie

Imaginez que vous gérez une boulangerie et que chaque jour vous devez préparer différents pains pour vos clients. Les étapes de ce processus ressemblent étonnamment au pipeline de rendu du navigateur :

Étape🥖 Métaphore de la boulangerieTravail réel du navigateurExemple concret
1. Préparer les ingrédientsOrganiser la liste des ingrédients (farine, œufs, crème...)Construire l'arbre DOM : analyser le HTML en une structure arborescenteVous écrivez <div><p>Hello</p></div>, le navigateur l'analyse en arbre div→p→"Hello"
2. Préparer les recettesOrganiser les fiches recettes (proportions pour chaque pain)Construire le CSSOM : analyser le CSS en un arbre de règlesVous écrivez .title { color: red }, le navigateur enregistre « le texte de .title est rouge »
3. Établir le planDécider quels pains faire aujourd'hui selon les ingrédients et recettesConstruire l'arbre de rendu : fusionner DOM et CSSOM, ne garder que les éléments visiblesLa balise <script> ne s'affiche pas, donc absente de l'arbre de rendu
4. Disposer les painsPlacer les pains dans la vitrine, décider où mettre chaque painLayout (mise en page) : calculer la taille et la position de chaque élémentCalculer « ce div fait 200px de large, 100px de haut, à la position (50, 50) de l'écran »
5. DécorerBadigeonner d'œuf, saupoudrer de sésame, ajouter de la crèmePaint (peinture) : « peindre » les couleurs, bordures, ombres des élémentsDessiner effectivement le « texte rouge » à l'écran
6. AssemblerSuperposer et disposer joliment tous les painsComposite (composition) : fusionner plusieurs couches en une image finaleLe GPU fusionne la couche de fond, la couche de texte, la couche d'image en une seule image

📊 Que pouvez-vous tirer de ce tableau ?

Parcourons ce tableau ligne par ligne pour comprendre chaque étape du pipeline de rendu :

Étapes 1-2 (phase de préparation) : le navigateur « comprend » d'abord votre code. Le HTML et le CSS sont analysés séparément car ils ont des rôles différents — le HTML définit « quel contenu », le CSS définit « à quoi ça ressemble ».

Étape 3 (phase de fusion) : pourquoi « fusionner » ? Parce que tous les éléments HTML ne sont pas affichés (par exemple <head>, <script>). Le navigateur doit combiner les « éléments visibles » avec « leurs styles » pour former un « plan de construction ».

Étapes 4-5 (phase de dessin) : le layout « calcule les positions », le paint « applique les couleurs ». Un changement de layout (comme modifier la largeur) entraîne un repaint, mais un changement de paint (comme modifier la couleur) n'entraîne pas de layout.

Étape 6 (phase de composition) : la « magie » des navigateurs modernes. L'approche traditionnelle est de « tout dessiner d'un coup » (CPU, lent), l'approche moderne est de « dessiner par couches + composer sur GPU » (rapide). C'est pourquoi une animation avec transform est plus fluide qu'avec width.

2.2 Les cinq étapes du pipeline de rendu

🏭渲染管线从代码到像素的五步旅程
想象你在印刷厂工作:稿件要排版、印刷、装订,最后才能变成书本。浏览器渲染网页也一样,HTML 和 CSS 要经过一道道"工序",才能变成屏幕上的画面。
🌲
构建DOM/CSSOM
解析代码
🎨
构建渲染树
合并筛选
📐
布局
计算位置
✏️
绘制
填充颜色
🔮
合成
合并图层
👆 点击上方任意阶段,查看详细解释
💡核心思想:每个阶段各司其职,前面的阶段为后面阶段准备数据。理解这个流程,你就能知道什么时候用什么方式修改页面,才能避免性能问题。

3. Première étape : construire l'arbre DOM et le CSSOM

3.1 Pourquoi « arbrifier » ?

🤔 Qu'est-ce que le DOM ?

Le DOM (Document Object Model, modèle d'objet de document) est une structure arborescente que le navigateur crée à partir du document HTML, pour permettre à JavaScript de manipuler facilement les éléments de la page.

Imaginez un arbre généalogique :

  • Tout en haut, l'« ancêtre » (<html>)
  • En dessous, les « enfants » (<body>, <head>)
  • Encore en dessous, les « petits-enfants » (<div>, <p>, <span>)

Pourquoi le transformer en arbre ? Parce qu'une structure arborescente facilite la « recherche » et la « modification ». Par exemple, pour trouver « tous les éléments avec la classe title », le navigateur peut chercher rapidement dans l'arbre au lieu de fouiller lentement dans un tas de texte désordonné.

Quand le navigateur reçoit le HTML, il ne l'affiche pas immédiatement — il doit d'abord le « comprendre ». Ce processus se déroule en trois étapes :

Première étape : analyse lexicale — découper le code en « mots »

html
<div class="container">
  <p>Hello World</p>
</div>

Le navigateur voit ce code et le « découpe » d'abord :

  • <div> → « balise ouvrante div »
  • class="container" → « attribut class, valeur container »
  • <p> → « balise ouvrante p »
  • Hello World → « contenu texte »
  • </p> → « balise fermante p »
  • </div> → « balise fermante div »

Deuxième étape : analyse syntaxique — assembler les « mots » en « nœuds »

Le navigateur assemble ces « mots » en « nœuds » selon les règles HTML :

  • Nœud élément : <div>, <p>
  • Nœud attribut : class="container"
  • Nœud texte : "Hello World"

Troisième étape : construire l'arbre — établir les relations parent-enfant

Enfin, le navigateur construit la structure arborescente selon les imbrications des balises :

Document (nœud racine du document)
└── html
    └── body
        └── div.class = "container"
            └── p
                └── "Hello World"

3.2 Le CSSOM : le « manuel des règles » de style

🤔 Qu'est-ce que le CSSOM ?

Le CSSOM (CSS Object Model, modèle d'objet CSS) est une structure arborescente que le navigateur crée à partir des règles CSS, utilisée pour calculer le style final de chaque élément.

Imaginez un guide d'assemblage vestimentaire :

  • Les règles du niveau supérieur (la police de body) affectent les niveaux inférieurs (tous les éléments enfants)
  • En cas de conflit (plusieurs règles spécifient des couleurs différentes pour le même élément), la « priorité » détermine laquelle s'applique
  • Au final, on calcule quels « vêtements » chaque élément doit porter

La construction du CSSOM est similaire à celle du DOM, avec une différence clé : le CSS est « hérité » et « en cascade » .

Voir le processus de construction du CSSOM

CSS original :

css
body {
  font-size: 16px;
  color: #333;
}

.container {
  width: 100%;
  color: red;  /* remplace la couleur de body */
}

.container p {
  font-weight: bold;
}

Arbre CSSOM construit :

StyleSheet
├── body
│   ├── font-size: 16px
│   └── color: #333
└── .container
    ├── width: 100%
    ├── color: red  (priorité plus élevée, remplace la couleur de body)
    └── p
        └── font-weight: bold

3.3 Pièges courants : pourquoi mon CSS ne « s'applique pas » ?

Piège n°1 : conflit de spécificité des sélecteurs CSS

Voir l'erreur courante
css
/* Votre CSS */
#header { color: red; }      /* sélecteur id, spécificité 100 */
.title { color: blue; }     /* sélecteur class, spécificité 10 */

/* HTML */
<div id="header" class="title">De quelle couleur est ce texte ?</div>

Vous pensez que c'est bleu, mais c'est rouge. Parce que la spécificité du sélecteur id (100) est supérieure à celle du sélecteur class (10).

Piège n°2 : balises HTML non fermées, le navigateur « corrige automatiquement »

Voir comment le navigateur corrige le HTML erroné
html
<!-- Votre HTML -->
<div>
  <p>Ceci est un texte
</div>

<!-- Après correction par le navigateur -->
<div>
  <p>Ceci est un texte</p>  <!-- Le navigateur ferme automatiquement la balise -->
</div>

Le navigateur est « indulgent » et corrige automatiquement vos erreurs. Mais cette indulgence a un coût — le navigateur doit faire des calculs supplémentaires pour deviner votre intention, ce qui impacte les performances.

🌲DOM到渲染树浏览器如何构建渲染树
浏览器需要把 HTML 和 CSS 合并成一棵"渲染树"。想象你在组装家具:图纸是 DOM,说明书是 CSSOM,只有结合两者,才能知道每个零件长什么样、放在哪里。
DOM树
<html>
<head>
<style>
<body>
<div>
<span>
<script>
+
CSSOM树
body
div
color: red
span
display: block
script
display: none
=
渲染树
div
span
可见节点
不可见节点(不包含在渲染树中)
💡核心要点:渲染树只包含可见的节点(display: none 的元素会被忽略)。每个渲染树节点都包含对应的 DOM 节点和计算出的样式信息。渲染树构建完成后,浏览器才能进入布局阶段。

4. Deuxième étape : construire l'arbre de rendu

4.1 Pourquoi a-t-on besoin de l'« arbre de rendu » ?

Vous vous demandez peut-être : « On a déjà l'arbre DOM et le CSSOM, pourquoi construire encore un arbre de rendu ? On ne peut pas utiliser directement le DOM ? »

La réponse est : l'arbre DOM contient trop d'informations « inutiles » .

Par exemple, ce HTML :

html
<html>
<head>
  <title>Titre de la page</title>
  <style>/* Code CSS */</style>
  <script>/* Code JavaScript */</script>
</head>
<body>
  <div class="container">
    <p>Contenu visible</p>
  </div>
  <div style="display: none">
    <p>Contenu caché (display:none)</p>
  </div>
</body>
</html>

L'arbre DOM contient tous les éléments :

  • <head>, <title>, <style>, <script> (qui ne s'affichent pas)
  • Le div avec display: none (qui ne s'affiche pas non plus)

Mais l'arbre de rendu ne contient que les éléments « à dessiner à l'écran » :

  • Supprime <head> et ses enfants
  • Supprime le div avec display: none

4.2 Règles de construction de l'arbre de rendu

Lors de la construction de l'arbre de rendu, le navigateur suit un ensemble de règles :

ScénarioTraitementExempleImpact sur les performances
display: noneComplètement exclu de l'arbre de renduL'élément et ses enfants sont invisibles✅ Réduit la charge de rendu
visibility: hiddenInclus dans l'arbre de rendu, mais non peintOccupe l'espace, mais totalement transparent⚠️ Nécessite quand même le calcul de layout
opacity: 0Inclus dans l'arbre de rendu, mais transparentInteractif (cliquable), mais invisible⚠️ Nécessite quand même le calcul de layout
Hors de la fenêtre d'affichageInclus dans l'arbre de rendu, pas encore peintPeint seulement lors du défilement dans la vue⚠️ Mais toujours dans l'arbre de rendu

📊 Que pouvez-vous tirer de ce tableau ?

Constat clé : display: none est la seule méthode de masquage qui « économise vraiment des performances », car l'élément est totalement absent de l'arbre de rendu — le navigateur ne fait aucun calcul de layout ni de peinture pour lui.

En revanche, visibility: hidden et opacity: 0, bien qu'« invisibles », restent dans l'arbre de rendu — le navigateur doit quand même calculer leur layout (ils occupent de l'espace). Si vous devez « masquer sans affecter le layout » (par exemple pour une animation de fondu), utilisez opacity ; si vous devez « masquer complètement sans occuper d'espace », utilisez display: none.

4.3 Pièges courants : pourquoi ma page rame-t-elle encore malgré display:none ?

❌ Idée reçue : croire que les éléments en display:none « n'existent pas »

Beaucoup pensent qu'avec display: none, l'élément « disparaît » et qu'aucune opération dessus n'affecte les performances. C'est faux !

Même si l'élément en display: none n'est pas dans l'arbre de rendu, quand vous modifiez ses propriétés via JavaScript, le navigateur doit quand même :

  1. Recalculer les styles (faire correspondre les règles CSS)
  2. Suivre les changements (se préparer à un affichage futur)

Voici un exemple d'« optimisation » erronée :

Voir le code de la « fausse optimisation »
javascript
// ❌ Ce que vous croyez être une « optimisation » : cacher, modifier, réafficher
const container = document.getElementById('list')
container.style.display = 'none'

// Manipulation intensive du DOM
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'  // Modifie la largeur !
  item.textContent = `Item ${i}`
  container.appendChild(item)
}

container.style.display = 'block'

// Problème : chaque modification de style.width oblige le navigateur à recalculer les styles,
// même si l'élément est en display:none !

✅ La bonne approche d'optimisation :

javascript
// Utiliser DocumentFragment pour les opérations par lots
const container = document.getElementById('list')
const fragment = document.createDocumentFragment()  // Conteneur virtuel

// Toutes les opérations se font sur le fragment en mémoire
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'
  item.textContent = `Item ${i}`
  fragment.appendChild(item)  // N'affecte pas le DOM réel
}

// Une seule insertion dans le DOM réel, un seul déclenchement de rendu
container.appendChild(fragment)

5. Troisième étape : layout et reflow

5.1 Qu'est-ce que le « layout » ?

🤔 Qu'est-ce que le layout ?

Le layout, aussi appelé reflow, est le processus par lequel le navigateur calcule « à quelle position et quelle taille » chaque élément de l'arbre de rendu doit apparaître.

Imaginez un architecte d'intérieur qui mesure une pièce :

  • Il mesure d'abord la longueur et la largeur de chaque pièce
  • Il décide où placer les meubles
  • Il calcule les coordonnées de chaque meuble

Pourquoi le layout est-il « coûteux » ? Parce que le changement d'un élément peut affecter les autres. Par exemple, si vous élargissez un div, le div d'à côté peut être poussé vers le bas, obligeant toute la page à se recalculer.

5.2 Les « mines » qui déclenchent un reflow

Voici les opérations courantes qui déclenchent un reflow, à collectionner et à mémoriser :

CatégoriePropriété/OpérationImpact performancesAlternative
Dimensionswidth, height, min/max-width/height💀💀💀Utiliser transform: scale()
Positiontop, right, bottom, left💀💀💀Utiliser transform: translate()
Margesmargin, padding💀💀Utiliser transform ou gap
Borduresborder-width💀💀Éviter les modifications fréquentes
ContenuChangement de texte, chargement d'image💀💀Réserver l'espace, éviter le layout shift
Policefont-size, line-height💀💀💀Éviter les modifications fréquentes
AffichageChangement de display💀💀💀Utiliser visibility ou opacity (si la disparition complète n'est pas nécessaire)
RequêteoffsetWidth, offsetHeight, etc.💀💀💀💀💀Lire par lots, éviter le layout thrashing

📊 Que pouvez-vous tirer de ce tableau ?

Constat clé :

  1. Les propriétés géométriques (largeur, hauteur, position) sont les plus coûteuses : elles déclenchent un calcul complet de layout
  2. Les propriétés de requête sont plus dangereuses que les modifications : lire offsetWidth force un layout synchrone (voir section 5.4)
  3. transform et opacity sont les plus performantes : elles ne déclenchent pas de reflow, seulement la composition

5.3 Pièges courants : pourquoi mon animation rame comme un diaporama ?

Piège : animer avec width

Voir le code d'animation peu performant
css
/* ❌ Mauvaise animation : déclenche un reflow */
.box {
  width: 100px;
  transition: width 0.3s;
}

.box:hover {
  width: 200px;  /* Changer la largeur déclenche un reflow ! */
}

Chaque image de l'animation déclenche un reflow, le navigateur doit :

  1. Recalculer la largeur
  2. Recalculer la position (peut affecter d'autres éléments)
  3. Repeindre

✅ Bonne animation : utiliser transform

css
/* ✅ Bonne animation : ne déclenche que la composition */
.box {
  width: 100px;
  transform: scaleX(1);
  transition: transform 0.3s;
}

.box:hover {
  transform: scaleX(2);  /* Le scale ne déclenche pas de reflow ! */
}

transform est géré directement par le GPU, ne déclenche ni reflow ni repaint — l'animation est fluide comme de la soie.

5.4 Tueur de performances : le layout synchrone forcé

💀 Le problème de performance le plus dangereux : le layout thrashing

Le layout synchrone forcé (Forced Synchronous Layout) , aussi appelé layout thrashing, est le problème de performance le plus courant et le plus grave.

Sa cause : quand JavaScript lit une propriété de layout (comme offsetWidth), le navigateur doit exécuter immédiatement le calcul de layout pour retourner une valeur exacte.

Si vous alternez lecture et écriture, le navigateur est obligé de faire « layout → lire → layout → lire » en boucle, créant un cercle vicieux.

Voir le code de layout thrashing
javascript
// ❌ Très mauvais : alternance lecture/écriture, provoque du layout thrashing
const elements = document.querySelectorAll('.item')

for (let i = 0; i < elements.length; i++) {
  const height = elements[i].offsetHeight  // Lecture → force le layout
  elements[i].style.width = (height * 2) + 'px'  // Écriture → marque pour reflow
  // La lecture de la prochaine itération force à nouveau le layout... cercle vicieux !
}

// Avec 100 éléments, cela déclenche 100 calculs de layout !

✅ La bonne approche d'optimisation : séparer lecture et écriture

javascript
const elements = document.querySelectorAll('.item')

// Étape 1 : lecture par lots (tout lire d'abord)
const heights = []
for (let i = 0; i < elements.length; i++) {
  heights.push(elements[i].offsetHeight)  // Un seul déclenchement de layout
}

// Étape 2 : écriture par lots (tout écrire ensuite)
requestAnimationFrame(() => {
  for (let i = 0; i < elements.length; i++) {
    elements[i].style.width = (heights[i] * 2) + 'px'  // Un seul déclenchement de reflow
  }
})
📐布局与重排看看布局计算如何影响页面
盒子
邻居元素
触发阶段:
性能影响:-
是否影响其他元素:-
💡核心要点:布局属性(如 width、margin)会触发重排,影响周围元素的位置。而 transform 只触发合成,在 GPU 上处理,不影响其他元素,性能更好。

6. Quatrième étape : paint et repaint

6.1 Qu'est-ce que le « paint » ?

🤔 Qu'est-ce que le paint ?

Le paint (peinture) est le processus par lequel le navigateur « dessine » réellement à l'écran les éléments dont le layout a été calculé.

Imaginez peindre une pièce :

  • Étape de layout = prendre les mesures, tracer les lignes
  • Étape de paint = vraiment peindre, poser le papier peint

Le paint est moins coûteux que le layout, mais pas gratuit. Des peintures fréquentes affectent quand même les performances, surtout pour les éléments complexes (ombres, dégradés, etc.).

6.2 Signaux qui déclenchent un repaint

Contrairement au reflow, le repaint ne concerne que les changements d'« apparence », pas de « géométrie » :

CatégoriePropriétéImpact performancesRemarque
Couleurcolor, background-color💀Le déclencheur de repaint le plus courant
Fondbackground-image, background-position💀💀Les images sont plus lentes que les couleurs unies
Bordureborder-color, border-style💀Changer la couleur ou le style de bordure
Textetext-decoration, text-shadow💀💀L'ombre est plus lente que le texte simple
Ombre de boîtebox-shadow💀💀💀Les ombres complexes sont très lentes
Arrondiborder-radius💀Changer la taille de l'arrondi
TransparenceopacitySpécial : ne déclenche pas de repaint, seulement la composition

📊 Que pouvez-vous tirer de ce tableau ?

Constat clé : opacity est spécial ! Comme transform, il ne déclenche pas de repaint, mais passe directement à l'étape de composition. C'est pourquoi les animations de fondu avec opacity sont les plus performantes.

De plus, les ombres et les dégradés sont plus coûteux que le repaint car ils nécessitent des calculs de pixels complexes. Si votre page a beaucoup de box-shadow, envisagez d'utiliser des pseudo-éléments ou des images à la place.

6.3 Pièges courants : pourquoi mon effet hover est-il saccadé ?

Piège : animer le hover avec box-shadow

Voir l'effet hover peu performant
css
/* ❌ Mauvais effet hover : l'animation de box-shadow est très lente */
.card {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: box-shadow 0.3s;
}

.card:hover {
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);  /* L'ombre est très lente ! */
}

box-shadow nécessite un calcul pixel par pixel, l'animation sera saccadée.

✅ Bonne pratique : utiliser transform ou un pseudo-élément

css
/* ✅ Bon effet hover : utiliser transform */
.card {
  transform: translateY(0);
  transition: transform 0.3s, box-shadow 0.3s;
}

.card:hover {
  transform: translateY(-4px);  /* Changer l'ombre seulement au hover, pas d'animation dessus */
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
🎨绘制层优化浏览器如何通过分层提升性能
🖼️背景层
📄内容层
卡片
触发新层的 CSS 属性:
transform: translate3d(0,0,0)任何3D变换都会创建新层
opacity配合transition使用时
position: fixed固定定位元素需要独立层
will-change: transform显式提示浏览器创建层
💡核心要点:浏览器把需要动画的元素提升到独立的 GPU 层,这样动画时只需要调整位置和透明度,不需要重绘。但不要滥用,每个层都会占用 GPU 内存。

7. Cinquième étape : composition et accélération GPU

7.1 Qu'est-ce que la « composition » ?

🤔 Qu'est-ce que la composition (Composite) ?

La composition est la « magie » des navigateurs modernes. Elle divise les différentes parties de la page en plusieurs couches (Layers) , puis utilise le GPU (processeur graphique) pour composer l'image finale en parallèle.

Imaginez les calques de Photoshop :

  • Méthode traditionnelle = tout dessiner sur un seul calque (CPU série, lent)
  • Méthode par composition = dessiner par calques, fusionner à la fin (GPU parallèle, rapide)

Pourquoi la composition est-elle rapide ? Parce que le GPU excelle dans les tâches parallèles comme la « composition d'images », jusqu'à des dizaines de fois plus vite que le CPU.

7.2 Quels éléments sont promus en « couche de composition » ?

Le navigateur promeut automatiquement certains éléments dans des couches de composition indépendantes. Voici les déclencheurs courants :

DéclencheurPropriété/Valeur CSSImpact performancesRemarque
Transformation 3Dtransform: translate3d(), rotate3d()✅✅✅Meilleure performance d'animation
Astuce d'accélération matérielletransform: translateZ(0)✅✅Surnommé « forcer l'accélération GPU »
Animation de transparenceopacity qui change (avec animation)✅✅✅Ne déclenche pas de repaint
Position fixeposition: fixedÉvite le re-layout lors du défilement
Will-Changewill-change: transform, opacity✅✅Crée la couche à l'avance, attention à la mémoire
Canvas/WebGL<canvas>, contenu WebGL✅✅Naturellement dans une couche indépendante
Video<video>✅✅Couche indépendante, évite les interférences

📊 Que pouvez-vous tirer de ce tableau ?

Constat clé : transform et opacity sont les propriétés d'animation les plus performantes car elles ne déclenchent ni reflow ni repaint — elles passent directement à la composition. C'est pourquoi les guides d'optimisation recommandent toujours « d'utiliser transform et opacity pour les animations ».

Mais attention : chaque couche de composition consomme de la mémoire GPU. Abuser de translateZ(0) peut faire exploser la mémoire (voir section 7.4).

7.3 Pièges courants : trop de couches de composition rendent la page plus lente ?

💀 Le piège de la sur-optimisation

Certains entendent dire que « l'accélération GPU, c'est rapide » et ajoutent transform: translateZ(0) à tous les éléments — résultat, la page est encore plus lente.

Cause du problème : Chaque couche de composition nécessite de stocker une « texture » (bitmap) dans la mémoire GPU. Si une page a 100 couches de composition, la mémoire GPU peut être saturée, entraînant des plantages sur les appareils bas de gamme ou un retour au rendu CPU.

Voir le code de « sur-optimisation »
css
/* ❌ Mauvaise pratique : activer l'accélération GPU sur tous les éléments */
.card { transform: translateZ(0); }
.button { transform: translateZ(0); }
.icon { transform: translateZ(0); }
/* ... 100 éléments avec cette règle ... */

/* Résultat : mémoire GPU saturée, page bloquée */

✅ Bonne pratique : utiliser à bon escient

css
/* Stratégie 1 : activer seulement pour les éléments qui ont vraiment besoin d'animation */
.card {
  transition: transform 0.3s ease;
}

.card:hover {
  transform: translateY(-5px);  /* Crée automatiquement une couche de composition */
}

/* Stratégie 2 : utiliser will-change pour indiquer au navigateur */
.card {
  will-change: transform;  /* Crée la couche à l'avance */
}

/* Stratégie 3 : supprimer après l'animation */
.card:not(:hover) {
  will-change: auto;  /* Libère la mémoire GPU */
}
🎬合成层演示浏览器渲染的最后阶段 - 图层合成
合成是浏览器渲染的最后一步。想象你在制作PPT动画:你已经准备好了所有图层,现在只需要调整它们的位置、透明度,然后把它们叠在一起显示出来。这就是合成要做的事情。
🖼️背景层
📄内容层
浮层
合成结果
🖼️
📄
💡核心要点:合成阶段在 GPU 上执行,只调整位置、透明度等,不重新绘制像素。因此 transform 和 opacity 动画性能最好,不会触发重排和重绘。

8. Boucle d'événements : le « don d'ubiquité » de JavaScript

🤔 Qu'est-ce que la boucle d'événements ?

La boucle d'événements (Event Loop) est le mécanisme qui permet à JavaScript d'être « asynchrone ». Comme JavaScript est mono-thread (une seule chose à la fois), mais doit gérer les clics utilisateur, les requêtes réseau, les timers, etc., il a besoin d'un « système d'ordonnancement » pour gérer ces tâches.

Imaginez un centre de tri de colis :

  • Call Stack (pile d'appels) = le colis en cours de traitement
  • Web APIs = les entrepôts partenaires externes (timers, requêtes réseau, etc.)
  • Callback Queue (file de rappels) = l'étagère des colis en attente
  • Event Loop (boucle d'événements) = le robot de tri (vérifie constamment « puis-je traiter la tâche suivante ? »)

8.1 Macro-tâches et micro-tâches

Au début, JavaScript n'avait qu'une seule file de tâches. Mais avec la complexification de la programmation asynchrone, le navigateur a introduit deux types de tâches :

TypeSources courantesPrioritéMoment d'exécution
Macro-tâchesetTimeout/setInterval, opérations I/O, rendu UIBasseUne par cycle de boucle d'événements
Micro-tâchePromise.then, MutationObserverHauteJuste après la macro-tâche courante, toutes les micro-tâches sont vidées immédiatement

La « formule mnémotechnique » de l'ordre d'exécution :

1. Exécuter la macro-tâche courante (par exemple le bloc <script> entier)
2. Exécuter toutes les micro-tâches générées pendant l'exécution (Promise.then, etc.)
   ↳ Les micro-tâches peuvent générer de nouvelles micro-tâches, toutes sont vidées avant de continuer
3. Si nécessaire, effectuer le rendu UI (reflow/repaint)
4. Démarrer le prochain cycle de boucle d'événements, exécuter la prochaine macro-tâche

8.2 Pièges courants : Promise est plus rapide que setTimeout ?

❌ Idée reçue : setTimeout(fn, 0) s'exécute « immédiatement »

Beaucoup pensent que setTimeout(fn, 0) signifie « exécuter immédiatement après 0 milliseconde ». C'est une compréhension erronée.

En réalité, setTimeout(fn, 0) signifie : « après au moins 0 milliseconde, ajouter le rappel dans la file de macro-tâches » . Mais il doit attendre que la pile d'appels courante soit vide, que la file de micro-tâches soit vidée, et qu'un éventuel rendu UI soit terminé, avant de pouvoir s'exécuter.

Voir l'ordre d'exécution
javascript
console.log('1. Start')

setTimeout(() => {
  console.log('2. setTimeout callback')
}, 0)

Promise.resolve().then(() => {
  console.log('3. Promise.then')
})

console.log('4. End')

// Ordre de sortie auquel vous vous attendez :
// 1. Start
// 4. End
// 2. setTimeout callback  ← setTimeout(0) n'est-il pas immédiat ?
// 3. Promise.then

// Ordre de sortie réel :
// 1. Start
// 4. End
// 3. Promise.then         ← Promise.then s'exécute avant setTimeout !
// 2. setTimeout callback

Diagramme du flux d'exécution :

Pile d'appels (Call Stack)      File de macro-tâches          File de micro-tâches
                                [setTimeout callback]         [Promise.then callback]

1. console.log('1. Start')
   → Sortie : 1. Start

2. setTimeout(fn, 0)
   → Ajoute le rappel dans la file macro  ← [setTimeout callback]

3. Promise.resolve().then()
   → Ajoute le rappel dans la file micro                            ← [Promise.then callback]

4. console.log('4. End')
   → Sortie : 4. End

5. Pile d'appels vide, vérifie la file de micro-tâches
   → Trouve le rappel Promise.then
   → Exécute : console.log('3. Promise.then')
   → Sortie : 3. Promise.then

6. File de micro-tâches vidée
   → Peut nécessiter un rendu UI (si des changements ont eu lieu)

7. Vérifie la file de macro-tâches
   → Trouve le rappel setTimeout
   → Exécute : console.log('2. setTimeout callback')
   → Sortie : 2. setTimeout callback

💡 Leçon clé

Les micro-tâches sont « plus urgentes » que les macro-tâches. Si vous voulez qu'une opération s'exécute « juste après le bloc de code courant, mais avant la mise à jour UI », utilisez Promise.then ou queueMicrotask.

setTimeout(0) ne garantit pas une exécution immédiate — il est au minimum retardé jusqu'à ce que la pile d'appels soit vide et la file de micro-tâches vidée.

Event Loop: How JavaScript Executes Code

Code queue

1
console.log("1")
Executing
2
setTimeout(() => console.log("2"), 0)
3
console.log("3")
4
fetch("/api").then(() => console.log("4"))
5
console.log("5")

Worker (single thread)

👨‍💻
Running
Run console.log("1")

Task queue

No pending tasks

Output log

Waiting for output...

Execution order: not started

Written order: 1, 2, 3, 4, 5

Code is written top to bottom, but it does not always run top to bottom because async work is delayed until the current code finishes.

🔄宏任务与微任务事件循环中的任务优先级
JavaScript 是单线程的,但可以通过任务队列实现异步。就像餐厅只有一个厨师,但他可以同时处理多个订单:先做VIP订单(微任务),再做普通订单(宏任务)。
主线程(执行栈)
同步代码
任务队列
微任务队列(优先级高)
Promise.then()
queueMicrotask()
宏任务队列(优先级低)
setTimeout()
setInterval()
I/O 操作
代码示例
console.log('1')

setTimeout(() => console.log('2'), 0)  // 宏任务

Promise.resolve().then(() => console.log('3'))  // 微任务

console.log('4')

// 输出顺序:1 → 4 → 3 → 2
💡核心要点:每次宏任务执行完后,会清空所有微任务,然后再执行下一个宏任务。这就是为什么 Promise.then() 比 setTimeout() 先执行。

9. Optimisation des performances en pratique : faites « voler » votre page web

Maintenant que vous comprenez le pipeline de rendu, voyons comment optimiser. Voici les cinq techniques d'optimisation les plus pratiques.

9.1 Règle d'or : éviter le layout synchrone forcé

Problème : alterner lecture et écriture des propriétés de layout, provoquant du layout thrashing.

Voir la comparaison avant/après optimisation
javascript
// ❌ Très mauvais : alternance lecture/écriture, provoque du layout thrashing
for (let i = 0; i < elements.length; i++) {
  const height = elements[i].offsetHeight  // Lecture → force le layout
  elements[i].style.height = (height * 2) + 'px'  // Écriture → marque pour reflow
  // La lecture de la prochaine itération force à nouveau le layout... cercle vicieux !
}

// ✅ Très bon : tout lire d'abord, tout écrire ensuite
// Étape 1 : lecture par lots
const heights = []
for (let i = 0; i < elements.length; i++) {
  heights.push(elements[i].offsetHeight)
}

// Étape 2 : écriture par lots
requestAnimationFrame(() => {
  for (let i = 0; i < elements.length; i++) {
    elements[i].style.height = (heights[i] * 2) + 'px'
  }
})

9.2 Utiliser transform et opacity pour les animations

Problème : animer avec width, height, left, top déclenche des reflow.

Voir la comparaison avant/après optimisation
css
/* ❌ Mauvaise animation : déclenche un reflow */
.box {
  transition: width 0.3s, left 0.3s;
}
.box.moving {
  width: 200px;
  left: 100px;
}

/* ✅ Bonne animation : ne déclenche que la composition */
.box {
  transition: transform 0.3s;
}
.box.moving {
  transform: translateX(100px) scaleX(2);
}

9.3 Défilement virtuel : résoudre les grandes listes de données

Problème : quand le nombre d'éléments d'une liste atteint des milliers, le nombre de nœuds DOM devient excessif et dégrade les performances.

Idée centrale : ne rendre que les éléments visibles dans la fenêtre d'affichage (avec une petite marge tampon), le nombre de nœuds DOM reste fixe, indépendant de la quantité totale de données.

渲染性能优化让页面丝滑流畅的秘诀
渲染性能优化的目标是每秒60帧(16.67ms/帧)。就像拍电影,每秒帧数越多,画面越流畅。超过这个时间,用户就会感觉卡顿。
❌ 不好的做法
// 触发重排和重绘
function animate() {
element.style.width = '100px'
element.style.height = '100px'
requestAnimationFrame(animate)
}
性能开销
VS
✅ 优化做法
/* 只触发合成 */
function animate() {
element.style.transform = 'translate3d(0,0,0)'
requestAnimationFrame(animate)
}
性能开销
黄金法则:
1️⃣优先使用 transformopacity 做动画
2️⃣避免频繁读取布局属性(如 offsetWidth)
3️⃣使用 will-change 提前告知浏览器
💡核心要点:渲染路径越长,性能越差。最佳路径是:合成(Composite)> 重绘(Paint)> 布局(Layout)> 样式计算(Style)。尽量让动画停留在"合成"阶段,在 GPU 上完成。
Voir l'implémentation du défilement virtuel
vue
<template>
  <div class="virtual-list" @scroll="handleScroll">
    <!-- Élément fantôme pour dimensionner la barre de défilement -->
    <div class="phantom" :style="{ height: totalHeight + 'px' }"></div>

    <!-- Éléments de liste réellement rendus -->
    <div class="content" :style="{ transform: `translateY(${offsetY}px)` }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  items: Array,
  itemHeight: { type: Number, default: 50 }
})

const scrollTop = ref(0)
const buffer = 5  // Nombre d'éléments tampons

// Nombre d'éléments visibles dans la zone d'affichage
const visibleCount = computed(() => 10)

// Index de départ
const startIndex = computed(() =>
  Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - buffer)
)

// Index de fin
const endIndex = computed(() =>
  Math.min(props.items.length, startIndex.value + visibleCount.value + buffer * 2)
)

// Données actuellement visibles
const visibleItems = computed(() =>
  props.items.slice(startIndex.value, endIndex.value)
)

// Hauteur totale
const totalHeight = computed(() => props.items.length * props.itemHeight)

// Décalage
const offsetY = computed(() => startIndex.value * props.itemHeight)

const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop
}
</script>

9.4 Debounce et throttle : réduire la fréquence de déclenchement des événements

Problème : les événements déclenchés fréquemment (comme scroll, resize) dégradent les performances.

Voir l'implémentation du debounce et du throttle
javascript
// Debounce : retarde l'exécution ; si redéclenché pendant le délai, le timer est réinitialisé
function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// Throttle : exécute à intervalles de temps fixes
function throttle(fn, interval) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= interval) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

// Exemple d'utilisation
window.addEventListener('scroll', debounce(handleScroll, 200))
window.addEventListener('resize', throttle(handleResize, 100))

9.5 Lazy loading : charger les ressources non critiques en différé

Problème : trop de ressources chargées au premier écran ralentissent l'ouverture de la page.

Voir l'implémentation du lazy loading
javascript
// Lazy loading des images
const lazyImages = document.querySelectorAll('img[data-src]')

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src  // Charge l'image réelle
      img.removeAttribute('data-src')
      observer.unobserve(img)  // Arrête d'observer
    }
  })
})

lazyImages.forEach(img => imageObserver.observe(img))

10. Les problèmes de performance que vous devriez maintenant savoir identifier

Après avoir compris le pipeline de rendu du navigateur, vous devriez pouvoir identifier les problèmes de performance courants suivants :

Code problématiqueProblèmeComment le décrire à l'IA
element.style.width = ...Modification fréquente de la largeur dans une boucle« Cela déclenche de multiples reflows, veuillez utiliser transform ou un traitement par lots »
height = element.offsetHeightLecture d'une propriété de layout juste après une écriture« C'est un layout synchrone forcé, veuillez séparer les opérations de lecture et d'écriture »
element.className = ...Modification fréquente de class qui déclenche un recalcul de style« Utilisez classList.add/remove pour réduire les calculs de style »
Animation avec width/leftDéclenche reflow et repaint, performances médiocres« Utilisez transform et opacity pour les animations »
translateZ(0) sur tous les élémentsAbus de l'accélération GPU, explosion mémoire« Activez l'accélération GPU uniquement pour les éléments qui en ont besoin »
Liste de 10000 éléments tous rendusTrop de nœuds DOM, provoque des saccades« Implémentez le défilement virtuel, ne rendez que la zone visible »
Manipulation du DOM dans l'événement scrollFréquence de déclenchement trop élevée, provoque des saccades« Optimisez avec requestAnimationFrame ou throttle »
Animation hover avec box-shadowLe calcul d'ombre complexe est très lent« Utilisez transform ou un pseudo-élément, évitez d'animer box-shadow »

Si vous avez lu attentivement les « pièges courants » de chaque chapitre, vous maîtrisez aussi ces concepts fondamentaux :

  • Les cinq étapes du pipeline de rendu : DOM/CSSOM → Arbre de rendu → Layout → Paint → Composition
  • Reflow vs Repaint : le reflow est le plus coûteux (changement géométrique), le repaint l'est moins (changement d'apparence)
  • Layout synchrone forcé : l'alternance lecture/écriture provoque du layout thrashing, il faut les séparer
  • Accélération GPU : transform et opacity sont gérés par le GPU, performances optimales
  • Boucle d'événements : JavaScript est mono-thread, l'asynchrone est réalisé via des files de tâches

Ces concepts vous aideront à localiser rapidement les goulets d'étranglement de performance.

💡 Voici comment parler à l'IA face à un problème de performance

  • « L'animation est saccadée, vérifiez si elle déclenche un reflow ou un repaint »
  • « Les performances de défilement sont mauvaises, il faut peut-être utiliser throttle ou requestAnimationFrame »
  • « La liste rame avec beaucoup de données, il faut un défilement virtuel »
  • « Les modifications fréquentes de style dégradent les performances, optimisez avec transform »

11. Résumé : l'essence de l'optimisation du pipeline de rendu

À travers cet article, nous pouvons tirer les conclusions fondamentales suivantes :

Du point de vue pratique : ce n'est pas « plus d'optimisations » qui compte, mais des optimisations « bien ciblées ». Comprendre le pipeline de rendu du navigateur vous permet de savoir où concentrer vos efforts et où les relâcher.

Du point de vue des coûts :

  • La plupart des gaspillages de performance viennent de l'alternance fréquente lecture/écriture des propriétés de layout, à résoudre par la séparation lecture/écriture et le traitement par lots
  • Les animations complexes qui déclenchent reflow et repaint sont souvent dues à l'utilisation de « mauvaises propriétés », à résoudre par transform et opacity
  • Face au rendu de listes avec beaucoup de données, le DOM virtuel seul ne suffit plus, il faut combiner avec des techniques comme le défilement virtuel

L'objectif est : dans des conditions données de navigateur et de matériel, faire en sorte que chaque étape de rendu apporte un bénéfice de performance clair.


12. Tableau des termes

Terme anglaisCorrespondance françaiseExplication
DOMDocument Object ModelStructure arborescente formée par le navigateur après l'analyse du document HTML ; JavaScript peut manipuler les éléments de la page via l'API DOM
CSSOMCSS Object ModelStructure arborescente formée par le navigateur après l'analyse du CSS, combinée avec le DOM pour calculer le style final
Render TreeArbre de renduFusion de l'arbre DOM et du CSSOM, ne contient que les nœuds visibles, utilisé pour le calcul de layout et le paint
LayoutMise en pageProcessus de calcul des informations géométriques (position, taille) de chaque nœud de l'arbre de rendu, aussi appelé Reflow
ReflowReflow / RéagencementQuand les propriétés géométriques d'un élément (taille, position) changent, le navigateur doit recalculer la mise en page
PaintPeinture / RepaintProcessus de dessin à l'écran des styles d'éléments (couleurs, fonds, bordures, etc.) après le calcul de layout
RepaintRepaintMise à jour de la peinture déclenchée quand les propriétés d'apparence (couleur, fond) changent sans affecter la géométrie
CompositeCompositionProcessus de fusion de plusieurs couches de peinture (Layers) en une image écran finale, généralement exécuté sur le GPU
LayerCouche / Couche de compositionSurface de dessin indépendante créée par le navigateur pour optimiser le rendu, pouvant être transformée et composée séparément
Event LoopBoucle d'événementsMécanisme d'exécution asynchrone de JavaScript, responsable de l'ordonnancement des macro-tâches et micro-tâches
Call StackPile d'appelsStructure de données qui enregistre la fonction JavaScript en cours d'exécution
Macro TaskMacro-tâcheType de tâche de priorité basse dans la boucle d'événements, comme setTimeout, setInterval, opérations I/O, etc.
Micro TaskMicro-tâcheType de tâche de priorité haute dans la boucle d'événements, comme Promise.then, MutationObserver, etc.
Forced Synchronous LayoutLayout synchrone forcéProblème de performance où l'alternance lecture/écriture des propriétés de layout en JavaScript force le navigateur à exécuter immédiatement le calcul de layout
Layout ThrashingLayout thrashingPhénomène de dégradation brutale des performances causé par des layouts synchrones forcés répétés
Virtual ScrollingDéfilement virtuelTechnique qui ne rend que les éléments de liste visibles dans la fenêtre d'affichage, pour optimiser les performances des grandes listes
RAFrequestAnimationFrameAPI du navigateur pour exécuter du code JavaScript d'animation avant le prochain repaint