frontend / css
Performance de rendu : reflow, repaint et containment
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-changecorrectement, sans épuiser la mémoire GPU - Isoler une sous-arborescence avec
containetcontent-visibilitypour 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ées | Coût |
|---|---|---|
width, top, font-size | Layout + Paint + Composite | élevé |
background, color, box-shadow | Paint + Composite | moyen |
transform, opacity | Composite seul | faible |
É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.
/*
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 */
}/* 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 */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 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 */
}// 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ée | Coût |
|---|---|
width, top, font-size | Layout + Paint + Composite |
background, color, box-shadow | Paint + Composite |
transform, opacity | Composite seul |
Résumé
transform/opacitysont les propriétés les moins coûteuses à animer : elles évitent Layout et Paint.will-changedoit être appliqué juste avant l'animation et retiré après, jamais posé en permanence partout.content-visibility: autoaccé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
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.