Chapitre 80 : Optimisation du rendu : contain et content-visibility
Isolation des sous-arbres DOM pour améliorer les performances de peinture et réduire le coût du recalcul du layout.
Le moteur de rendu d'un navigateur (comme Blink ou Gecko) doit normalement traiter la page comme un arbre interdépendant. Si un élément change de taille, cela peut potentiellement déplacer tous les autres éléments de la page, déclenchant un "Reflow" (recalcul du layout) global. Pour les applications web modernes et complexes, ce comportement est un goulot d'étranglement majeur.
Les propriétés contain et content-visibility permettent de briser cette interdépendance en créant des frontières d'isolation.
L'isolation avec la propriété contain
1. Quoi
La propriété contain permet de définir un contrat de confinement entre un élément et le reste du document. Elle indique au navigateur que le contenu de l'élément est indépendant du reste de la page pour certaines étapes du pipeline de rendu.
Il existe quatre types de confinement :
layout: Les changements de layout à l'intérieur de l'élément n'affectent pas le layout extérieur, et inversement.paint: Le contenu de l'élément ne peut pas être dessiné en dehors de son bordure (similaire àoverflow: hidden, mais plus performant car il permet au navigateur de ne pas peindre l'élément s'il est hors écran).size: La taille de l'élément ne dépend pas de son contenu. L'élément est traité comme s'il avait une taille fixe (souvent0si aucune dimension n'est spécifiée), évitant ainsi de calculer la taille des enfants pour déterminer la taille du parent.strict: Raccourci pourcontain: size layout paint;.
2. Pourquoi
Dans une application complexe (ex: un dashboard avec des milliers de nœuds DOM), modifier une seule propriété CSS sur un petit composant peut forcer le navigateur à recalculer la position de chaque élément de la page.
En utilisant contain, on transforme un problème de complexité globale en une série de problèmes locaux. Si un élément est marqué comme contain: layout, le navigateur sait qu'il peut limiter le recalcul du layout à l'intérieur de ce sous-arbre uniquement.
3. Comment
A. Syntaxe de base
.component {
/* Isole le layout et la peinture */
contain: layout paint;
}
.widget {
/* Isolation totale : le navigateur ne regarde même pas
le contenu pour calculer la taille du widget */
contain: strict;
}
B. Cas concret : Composant de flux de données haute fréquence
Imaginons un widget de monitoring qui met à jour des valeurs numériques plusieurs fois par seconde. Sans isolation, chaque mise à jour pourrait déclencher un recalcul global.
/* Le conteneur du widget est isolé */
.monitoring-card {
width: 300px;
height: 200px;
/*
'strict' est idéal ici car nous avons des dimensions fixes.
Le navigateur sait que peu importe ce qui change à l'intérieur
de la carte, la carte elle-même fera toujours 300x200px
et ne débordera jamais.
*/
contain: strict;
border: 1px solid #ccc;
padding: 1rem;
background: #fff;
}
.data-value {
font-family: monospace;
font-weight: bold;
/* Les changements ici restent confinés à .monitoring-card */
}
C. Limitations
contain: sizeest dangereux : si vous ne définissez pas explicitement unewidthet uneheightsur l'élément, celui-ci s'effondrera à une taille de0x0car le navigateur ignore délibérément la taille de ses enfants.- Accessibilité : L'isolation peut parfois interférer avec certains calculs de positionnement pour les technologies d'assistance si elle est mal utilisée.
- Positionnement : Un élément avec
contain: layoutdevient la référence pour les éléments positionnés enabsoluteà l'intérieur, agissant comme unposition: relative.
4. Zone de Danger
❌ Erreur commune : Appliquer contain: strict sur un élément dont la hauteur doit s'adapter au contenu.
Résultat : L'élément disparaît ou s'écrase car size ignore le contenu.
✅ Bonne pratique : Utiliser contain: layout paint pour les composants dont la taille est dynamique mais dont le contenu ne doit pas affecter le reste de la page.
Impact de l'isolation du rendu via contain
L'optimisation avec content-visibility
1. Quoi
content-visibility est une propriété CSS qui permet de sauter complètement le rendu (layout et peinture) des éléments qui ne sont pas actuellement visibles à l'écran.
Elle possède deux valeurs principales :
visible: Comportement par défaut.hidden: L'élément est rendu comme s'il n'existait pas pour le layout et la peinture, mais il reste présent dans le DOM et accessible (contrairement àdisplay: none).auto: Le navigateur décide dynamiquement. Si l'élément est hors écran, il applique le comportement dehidden. Dès qu'il approche de la zone visible, il déclenche le rendu.
2. Pourquoi
Le coût du rendu d'une page n'est pas seulement lié au nombre de pixels, mais au nombre de nœuds DOM à traiter. Sur des pages très longues (ex: fils d'actualité, documentation massive), le navigateur passe un temps considérable à calculer le layout d'éléments que l'utilisateur ne verra peut-être jamais.
content-visibility: auto permet d'obtenir des gains de performance massifs en réduisant drastiquement le temps de "First Input Delay" (FID) et le temps de chargement initial.
3. Comment
A. Syntaxe de base
.section-hors-ecran {
content-visibility: auto;
/* Obligatoire pour éviter les sauts de scroll */
contain-intrinsic-size: 0 500px;
}
B. Cas concret : Liste de composants complexes
Imaginez une page de documentation avec 50 sections, chacune contenant des graphiques et des tableaux complexes.
.doc-section {
/*
Le navigateur ne calculera le layout et ne peindra
la section que lorsqu'elle sera proche du viewport.
*/
content-visibility: auto;
/*
CRITIQUE : On donne une estimation de la taille de la section.
Sans cela, le navigateur considère que la section fait 0px de haut
tant qu'elle est invisible. Quand elle devient visible, elle
"saute" soudainement à sa taille réelle, provoquant un
saut brutal de la barre de scroll (Layout Shift).
*/
contain-intrinsic-size: 1px 800px;
margin-bottom: 2rem;
padding: 2rem;
border-bottom: 1px solid #eee;
}
C. Limitations
- Recherche (Ctrl+F) : Les éléments avec
content-visibility: autosont toujours trouvables via la recherche du navigateur, mais le navigateur devra déclencher le rendu de l'élément pour y déplacer le focus. contain-intrinsic-size: C'est la partie la plus difficile. Si l'estimation est trop éloignée de la réalité, l'expérience utilisateur sera dégradée par des sauts de page.
4. Zone de Danger
❌ Erreur commune : Utiliser content-visibility: auto sans contain-intrinsic-size.
Résultat : La barre de défilement change de taille et de position frénétiquement pendant que l'utilisateur scrolle.
✅ Bonne pratique : Utiliser contain-intrinsic-size: auto 500px. La valeur auto permet au navigateur de mémoriser la taille réelle de l'élément après son premier rendu, afin de l'utiliser pour les rendus suivants.
Questions clés
1. Quelle est la différence fondamentale entre contain: paint et overflow: hidden ?
Découvrir la réponse
Bien que les deux empêchent le contenu de déborder visuellement, contain: paint informe le moteur de rendu que l'élément peut être totalement ignoré s'il est hors écran. overflow: hidden masque le débordement, mais le navigateur peut toujours calculer et peindre le contenu interne, même s'il n'est pas visible.
2. Pourquoi contain: strict nécessite-t-il des dimensions explicites ?
Découvrir la réponse
Parce que strict inclut contain: size. Le contrat de size stipule que la taille de l'élément ne dépend pas de son contenu. Si vous ne donnez pas de taille, le navigateur ne peut pas "demander" aux enfants leur taille pour remplir le parent, et le résultat par défaut est 0x0.
3. content-visibility: auto est-il identique à un lazy-loading de composants JS ?
Découvrir la réponse
Non. Le lazy-loading JS retarde l'insertion des nœuds dans le DOM. content-visibility: auto laisse les nœuds dans le DOM (préservant le SEO et l'accessibilité), mais demande au moteur de rendu de sauter les étapes coûteuses de Layout et de Paint jusqu'à ce que l'élément soit nécessaire.
4. Quel est l'impact de content-visibility sur le Cumulative Layout Shift (CLS) ?
Découvrir la réponse
S'il est utilisé sans contain-intrinsic-size, il augmente massivement le CLS car les éléments passent d'une taille de 0 à leur taille réelle lors du scroll. Avec une estimation correcte via contain-intrinsic-size, l'impact est minimisé.
Mise en pratique
Exercice 1 : Reproduction guidée
Objectif : Créer un conteneur de widget isolé.
Code de départ :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>contain - Exercice 1</title>
<style>
body { font-family: sans-serif; padding: 2rem; background: #eee; }
.page-layout {
display: flex;
gap: 1rem;
}
.widget-container {
width: 200px;
height: 200px;
background: #f0f0f0;
border: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
/* TODO : Appliquer l'isolation maximale ici */
}
/* Spinner animé — représente une animation interne au widget */
.spinner {
width: 40px;
height: 40px;
border: 4px solid #ccc;
border-top-color: #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.sidebar {
flex: 1;
padding: 1rem;
background: #fff;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div class="page-layout">
<div class="widget-container">
<div class="spinner"></div>
</div>
<aside class="sidebar">
<p>Contenu principal de la page. Les animations du widget
ne devraient pas déclencher de recalcul de layout ici.</p>
</aside>
</div>
</body>
</html>
Consigne :
- Ajoutez
contain: strictsur.widget-container(les dimensions sont fixes : 200×200px). - Ouvrez l'onglet Performance des DevTools et enregistrez quelques secondes — vérifiez que le spinner ne provoque pas de Layout dans le graphe de flamme.
Découvrir la solution commentée
.widget-container {
width: 200px;
height: 200px;
/*
L'isolation 'strict' est parfaite ici car nous avons
des dimensions fixes. Cela isole le layout, la peinture
et la taille.
*/
contain: strict;
background: #f0f0f0;
border: 1px solid #ccc;
}
Exercice 2 : Adaptation
Objectif : Optimiser une liste de cartes dynamiques.
Vous avez une liste de cartes .card dont la hauteur varie selon le texte. Vous voulez optimiser le rendu pour les cartes hors écran, mais vous ne voulez pas que la page "saute" lors du scroll.
Code de départ :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>content-visibility - Exercice 2</title>
<style>
body { font-family: sans-serif; padding: 1rem; }
.card {
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
/* TODO : Ajouter content-visibility et contain-intrinsic-size */
}
.card h2 { margin: 0 0 0.5rem; font-size: 1.1rem; }
.card p { margin: 0; color: #555; }
</style>
</head>
<body>
<!-- Générer ~50 cartes pour voir l'effet du rendu différé -->
<div class="card"><h2>Article 1</h2><p>Résumé court.</p></div>
<div class="card"><h2>Article 2</h2><p>Un résumé un peu plus long qui prend plusieurs lignes et fait varier la hauteur de la carte.</p></div>
<div class="card"><h2>Article 3</h2><p>Résumé court.</p></div>
<!-- ... répétez jusqu'à ~50 cartes pour observer le comportement lors du scroll ... -->
</body>
</html>
Consigne :
- Ajoutez
content-visibility: autosur.cardpour que le navigateur saute le rendu des cartes hors écran. - Ajoutez
contain-intrinsic-size: auto 300pxpour éviter les sauts de scroll (estimation initiale : 300px de hauteur). - Observez dans les DevTools → Rendering → cochez "Paint flashing" pour voir quelles zones sont peintes lors du scroll.
Découvrir la solution commentée
.card {
/*
On utilise 'auto' pour sauter le rendu hors écran.
*/
content-visibility: auto;
/*
On utilise 'auto' comme première valeur pour que le navigateur
mémorise la taille réelle après le premier rendu,
et '300px' comme estimation initiale.
*/
contain-intrinsic-size: auto 300px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
}
Exercice 3 : Conception
Objectif : Architecture de performance pour un Dashboard.
Code de départ :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>contain + content-visibility - Exercice 3</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; display: flex; height: 100vh; overflow: hidden; }
.sidebar {
width: 250px;
height: 100vh;
background: #2c3e50;
color: #fff;
padding: 1rem;
flex-shrink: 0;
/* TODO : Appliquer contain: strict ici */
}
.main-feed {
flex: 1;
overflow-y: auto;
padding: 1rem;
background: #f5f5f5;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.widget {
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
padding: 1rem;
min-height: 150px;
/* TODO : Appliquer content-visibility et contain: layout paint ici */
}
.widget-header { font-weight: bold; margin-bottom: 0.5rem; }
.data-value { font-family: monospace; font-size: 2rem; color: #3498db; }
</style>
</head>
<body>
<aside class="sidebar">
<h2>Dashboard</h2>
<nav>
<p>Métriques</p>
<p>Alertes</p>
<p>Paramètres</p>
</nav>
</aside>
<main class="main-feed">
<!-- Répéter ~20 widgets pour simuler un flux dense -->
<div class="widget">
<div class="widget-header">CPU Usage</div>
<div class="data-value">72%</div>
</div>
<div class="widget">
<div class="widget-header">Memory</div>
<div class="data-value">4.2 GB</div>
</div>
<div class="widget">
<div class="widget-header">Network In/Out</div>
<div class="data-value">1.2 MB/s</div>
</div>
<!-- ... 17 widgets supplémentaires ... -->
</main>
</body>
</html>
Consigne :
- La barre latérale ne change jamais de taille : appliquez
contain: strictsur.sidebar. - Chaque widget peut changer de valeur en temps réel : appliquez
contain: layout paintsur.widgetpour que les mises à jour internes ne déclenchent pas de recalcul sur tout le flux. - Les widgets hors écran ne doivent pas être rendus inutilement : ajoutez
content-visibility: autoetcontain-intrinsic-size: auto 400pxsur.widget.
Découvrir la solution commentée
/* 1. La barre latérale est statique et ne change jamais de taille */
.sidebar {
width: 250px;
height: 100vh;
contain: strict;
}
/* 2. Le flux central */
.main-feed {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 3. Les widgets */
.widget {
/*
On utilise 'auto' pour le rendu différé.
On utilise 'auto' dans intrinsic-size pour mémoriser la hauteur réelle.
*/
content-visibility: auto;
contain-intrinsic-size: auto 400px;
/*
On ajoute 'contain: layout paint' pour s'assurer que
les mises à jour de données internes au widget (ex: un graphe)
ne déclenchent pas un recalcul de tout le flux central.
*/
contain: layout paint;
}