Retour au cours

frontend / css

Performance de rendu : reflow, repaint et containment

Leçon 181 exercice

Explication

Ce que vous allez apprendre

  • Comprendre les quatre étapes du pipeline de rendu (Style, Layout, Paint, Composite)
  • Identifier quelles propriétés CSS déclenchent un reflow coûteux et lesquelles restent bon marché
  • Utiliser will-change correctement, sans épuiser la mémoire GPU
  • Isoler une sous-arborescence avec contain et content-visibility pour accélérer le rendu
  • Repérer et corriger un "layout thrashing" en JavaScript

Dans quel contexte ?

Le fil d'actualité d'un réseau social affiche des centaines de cartes, et le défilement devient saccadé sur mobile dès que la liste dépasse une cinquantaine d'éléments. En creusant, l'équipe technique découvre que chaque carte anime sa width au survol et que le JavaScript lit et écrit des dimensions en boucle. Cette leçon explique précisément pourquoi cela rame et comment corriger le problème.

Étape 1 : "ça marche" ne veut pas dire "c'est performant"

Écrire du CSS qui fonctionne visuellement ne garantit pas qu'il soit performant. Pour comprendre pourquoi certaines animations sont saccadées, il faut connaître le pipeline de rendu du navigateur.

Étape 2 : quatre étapes à chaque changement visuel

Ce pipeline comporte quatre étapes successives : Style, Layout, Paint, Composite. Le navigateur les exécute à chaque changement, mais leur coût varie ÉNORMÉMENT selon la propriété modifiée.

Étape 3 : la catégorie la plus coûteuse

Changer width ou top force le navigateur à TOUT recalculer depuis l'étape Layout — la position et la taille de potentiellement des centaines d'éléments doivent être réévaluées.

Propriété modifiéeÉtapes déclenchéesCoût
width, top, font-sizeLayout + Paint + Compositeélevé
background, color, box-shadowPaint + Compositemoyen
transform, opacityComposite seulfaible

Étape 4 : et les deux autres catégories

Changer color ou background évite le Layout mais nécessite quand même de redessiner les pixels (Paint). Changer transform ou opacity, en revanche, ne touche QUE la dernière étape (Composite), traitée directement par la carte graphique.

Étape 5 : will-change, un outil à double tranchant

Une fois cette hiérarchie comprise, voici un outil qui prévient le navigateur qu'une propriété va bientôt changer. Mais l'appliquer partout en permanence épuise la mémoire graphique et peut DÉGRADER les performances.

Étape 6 : la bonne pratique pour will-change

La bonne pratique consiste à l'activer juste avant l'animation et à le retirer juste après — un outil chirurgical, pas un réglage global.

Piège fréquent

Poser will-change: transform sur * ou sur des dizaines d'éléments en permanence force le navigateur à créer autant de calques graphiques dédiés, ce qui épuise la mémoire GPU et peut RALENTIR la page au lieu de l'accélérer. Réservez will-change à l'élément précis, juste avant l'animation.

Un dernier piège, le layout thrashing

Il reste un piège classique en JavaScript qui aggrave tout ça : alterner lecture et écriture de dimensions dans une boucle force un recalcul de layout à CHAQUE itération. Grouper toutes les lectures, puis toutes les écritures, évite cette cascade.

Et ensuite ?

Une fois la performance de rendu comprise, la prochaine étape explore les fonctionnalités CSS les plus récentes : nesting, @container, @scope et @property.

Commandes & code

Performance de rendu

Comprendre le pipeline de rendu du navigateur pour écrire un CSS qui ne rame pas.

css
/*
Pipeline de rendu, dans l'ordre :
  1. Style   : calcul des styles appliqués (cascade + spécificité)
  2. Layout  : calcul de la position/taille de CHAQUE boîte (= "reflow")
  3. Paint   : dessin des pixels (couleurs, ombres, texte) (= "repaint")
  4. Composite : assemblage des calques par le GPU

Modifier width/height/top/left/font-size -> refait TOUT depuis Layout (coûteux)
Modifier color/background      -> refait seulement depuis Paint
Modifier transform/opacity     -> ne touche QUE Composite -> le moins coûteux
*/

/* MAUVAIS : déclenche un reflow à chaque frame d'animation */
.mauvais {
    transition: width 0.3s, top 0.3s;
}

/* BON : reste dans le compositeur GPU */
.bon {
    transition: transform 0.3s;
    will-change: transform; /* prévient le navigateur -> peut pré-optimiser le calque */
}
css
/* will-change : puissant mais dangereux si mal utilisé */
.carousel-image {
    will-change: transform;
    /* crée son propre calque de composition -> évite de repeindre les voisins */
}

/* MAUVAIS : will-change sur TROP d'éléments épuise la mémoire GPU */
* { will-change: transform; } /* à ne jamais faire */

/* BON : appliqué juste avant l'animation, retiré juste après */
js
const image = document.querySelector(".carousel-image");

image.addEventListener("pointerenter", () => {
    image.style.willChange = "transform"; // prévient juste avant l'animation
});
image.addEventListener("transitionend", () => {
    image.style.willChange = "auto"; // libère la ressource GPU après coup
});
css
/* CSS containment : isole une sous-arborescence pour limiter la portée
   des recalculs de layout/paint aux seuls changements internes */
.widget-independant {
    contain: layout paint;
    /* layout : les changements internes n'affectent pas le layout externe
       paint  : le contenu ne dessine jamais hors de sa propre boîte */
}

.carte-liste {
    content-visibility: auto;
    /* ne calcule le layout/paint QUE si l'élément est proche du viewport --
       gain massif sur de longues listes (des centaines de cartes) */
    contain-intrinsic-size: auto 300px;
    /* taille estimée réservée pour les éléments pas encore rendus,
       évite un layout shift au scroll */
}
js
// Layout thrashing en JS : le piège classique à connaître même côté CSS
// MAUVAIS : lecture puis écriture en boucle -> force un reflow à CHAQUE itération
elements.forEach((el) => {
    const largeur = el.offsetWidth; // LECTURE -> force le navigateur à recalculer le layout
    el.style.width = largeur + 10 + "px"; // ÉCRITURE -> invalide le layout pour la lecture suivante
});

// BON : on sépare toutes les lectures, puis toutes les écritures
const largeurs = elements.map((el) => el.offsetWidth); // lectures groupées
elements.forEach((el, i) => {
    el.style.width = largeurs[i] + 10 + "px"; // écritures groupées
});
Propriété modifiéeCoût
width, top, font-sizeLayout + Paint + Composite
background, color, box-shadowPaint + Composite
transform, opacityComposite seul

Résumé

  • transform/opacity sont les propriétés les moins coûteuses à animer : elles évitent Layout et Paint.
  • will-change doit être appliqué juste avant l'animation et retiré après, jamais posé en permanence partout.
  • content-visibility: auto accélère drastiquement le rendu de longues listes hors écran.
  • Grouper toutes les lectures DOM puis toutes les écritures évite le "layout thrashing".

Exercices pratiques

1 disponible
1

Mission : un fil d'actualité qui rame sur mobile

Objectif : Diagnostiquer les causes de reflow répété dans un fil d'actualité et proposer les corrections CSS et JS adaptées.

Contexte

Sur fil-actualite.css, chaque carte .post anime width au survol (transition: width 0.3s;, passant de 95% à 100%). Le fil affiche plusieurs centaines de cartes, et un script associé lit el.offsetWidth puis écrit immédiatement el.style.width pour chacune, dans une seule boucle forEach. Le défilement devient saccadé dès que la liste dépasse une cinquantaine d'éléments visibles.

Résoudre l’exercice →