La performance de l'affichage est un élément important pour l'expérience utilisateur et le référencement.
Les pages web, notamment dans le commerce en ligne, peuvent comporter de nombreux éléments, blocs produits par exemple, en plus des habituels composants d'une page (bannière de haut de page, navigation, fil d'Ariane, etc.) et, parmis ceux-ci, le pied de page qui contient beaucoup d'informations.
Sur un écran de taille moyenne, ces blocs et ce pied de page ne sont pas dans la zone de rendu. Mais le navigateur va tout de même interpréter la mise en page. Normal.
La question est : est-ce bien utile ? Divulgâchons tout de suite : pas vraiment.
Mais alors comment faire ça simplement ?
CSS pour la performance
On peut utiliser une propriété CSS assez peu connue (mais ça va changer !), content-visibility. Elle accepte 3 valeurs :
visiblehiddenauto
Oublions vite hidden, puisque que c'est à peu près la même chose que display: none, ce qui peut poser de sérieux problèmes pour l'accessibilité.
Par contre, les deux autres nous intéressent fortement.
Pour la définition complète, c'est sur MDN. En association de cette propriété, on va également utiliser contain.
Un article de web.dev détaille et explique le fonctionnement.
Nativement, ça fonctionne très bien. Le petit défaut est que la gestion du rendu est lié au navigateur qui va décider à quel moment il dessine le bloc.
Selon la conception, il peut se produire un effet « pop up » au moment où le bloc entre dans le viewport, notamment au défilement de la page.
Prenons un bloc sur ce site comme sur la page photographie. Sur un mobile, on ne voit que le premier. Sur écran de bureau de taille moyenne, on ne voit que la première ligne, éventuellement le début de la seconde. Le reste est masqué, donc il est inutile de calculer le rendu graphique.
À partir du 4ème bloc, la classe content-visibility-auto est ajoutée. Cette classe est parlante…
.content-visibility-auto {
content-visibility: auto;
contain: style layout paint;
}
Maintenant, si on veut éviter l'effet « pop up », il va falloir un peu de Javascript.
Le script
Le script va récupérer les éléments au sein de la page qui possèdent la classe content-visibility-auto. Mais au lieu d'appliquer le rendu natif, il va définir des rootMargin qui seront utilisés par l'API IntersectionObserver.
En résumé, quand les blocs concernés entrent dans la zone définie, la classe content-visibility-auto est remplacée par content-visibility-visible avant même d'être « visible ».
.content-visibility-visible {
content-visibility: visible;
}
Note : par défaut, la marge haute et basse est égale à :
document.documentElement.offsetHeight / 2. Mais elle peut être configurée par bloc en ajoutant l'attribut data-root-margin="30px 0px" par exemple.
Taille brute : 1,97 kilo-octets (~796,00 octets gzip)
/**
* Initialise l'observation de visibilité pour les éléments avec content-visibility-auto.
* Les éléments deviennent visibles lorsqu'ils entrent dans le viewport en fonction de leur rootMargin.
* Une fois visibles, ils ne sont plus observés pour optimiser les performances.
*/
(() => {
document.addEventListener('DOMContentLoaded', () => {
/**
* Calcul de la valeur par défaut pour `rootMargin`.
* On utilise la moitié de la hauteur de la fenêtre pour déclencher l'observation
* un peu avant que l'élément n'entre complètement dans le viewport.
* @type {string}
*/
const defaultRootMargin = `${document.documentElement.offsetHeight / 2}px 0px`;
/**
* Map regroupant les éléments à observer en fonction de leur rootMargin.
* Permet d'optimiser les performances en évitant de créer un observer par élément.
* @type {Map<string, HTMLElement[]>}
*/
const elementsByRootMargin = new Map();
/* Sélection et regroupement des éléments par rootMargin */
document.querySelectorAll('[class*="content-visibility-auto"]').forEach(el => {
const rootMargin = el.dataset.rootMargin || defaultRootMargin;
if (!elementsByRootMargin.has(rootMargin)) {
elementsByRootMargin.set(rootMargin, []);
}
elementsByRootMargin.get(rootMargin).push(el);
});
/* Création des observers pour chaque groupe de rootMargin */
elementsByRootMargin.forEach((elements, rootMargin) => {
const visibilityObserver = new IntersectionObserver((entries) => {
entries.forEach(({target, isIntersecting}) => {
if (isIntersecting) {
target.classList.add('content-visibility-visible');
target.classList.remove('content-visibility-auto');
visibilityObserver.unobserve(target);
}
});
}, {rootMargin});
elements.forEach(el => visibilityObserver.observe(el));
});
});
})();
Charger ce script dans le head ou en pied de page voilà…
Conséquences
On peut constater une diminution importante du LCP, particulièrement mobile, et une expérience utilisateur grandement améliorée. Après plusieurs tests, en mode mobile le LCP moyen est inférieur à 2 secondes et de 0,5 seconde sur desktop, toujours sur la page photographie.
Le script a aussi un impact positif sur le CLS, bien que de manière indirecte et spécifique. Pour un impact maximal sur le CLS, il faudrait utiliser la propriété CSS contain-intrinsic-size en combinaison avec content-visibility, mais dans un monde responsive, c'est un peu plus compliqué, voir contre productif. Voir la documentation en anglais de contain-intrinsic-size.
Données Pagespeed pour cette page :
À noter que le navigateur réduit également sa consommation de ressources.
Compatibilité
content-visibility est pris en charge par la grande majorité des navigateurs. Sinon, la propriété est ignorée.
| Navigateur | Version minimale supportée | Notes |
|---|---|---|
| Chrome | 85 | Support complet depuis la version 85 |
| Edge (Chromium) | 85 | Identique à Chrome |
| Firefox | 125 | Nécessite l’activation manuelle avant la version 125 (about:config) |
| Safari | 18.1 | Support complet depuis Safari 18.1 |
| Opera | 71 | Support complet depuis la version 71 |
| Chrome Android | 85 | Support complet |
| Firefox Android | 124 | Support complet depuis la version 124 |
| Safari iOS | 18.1 | Support complet depuis iOS 18.1 |
| Samsung Internet | 14 | Support complet depuis la version 14 |
That's all, Folks !