CLS 0,377 → 0,002 en une journée : trois rondes de critical CSS
Layout shift 0,377 sur un site neuf. Le soir même - 0,002. Trois corrections : dimensions du logo, header-nav en critical CSS, Inter Fallback avec size-adjust:107%.
Le 22 mars 2026 j’ai lancé un nouveau site pour l’entreprise de nettoyage où je gère le marketing comme directeur marketing. Le premier passage Lighthouse mobile a affiché CLS 0,377 - zone rouge, le ranking se prend une coupe nette chez Google comme chez Yandex. Le soir même - 0,002. Trois corrections, sans réécrire le site.
En bref
- Au départ CLS 0,377. Après trois corrections critical CSS - 0,002. Une journée de travail.
- Correction n°1 : attributs
widthetheightsur le logo dans le header. ~40 % du décalage gagné.- Correction n°2 : règles de
.header-navdéplacées dans le critical CSS inline du<head>. Le header a arrêté de sauter.- Correction n°3 : police de secours Inter Fallback basée sur Arial avec
size-adjust:107%etascent-override:90%. Le FOUT a arrêté de pousser les textes à la fin du chargement woff2.
C’est quoi le CLS et pourquoi 0,377 c’est mauvais
Cumulative Layout Shift c’est une métrique que Google a introduite en 2020 dans les Core Web Vitals. Elle compte combien de fois et avec quelle amplitude le contenu de la page saute pendant le chargement. Un saut, c’est quand un bloc de texte ou une image se déplace après que l’utilisateur voit la page, et pas à cause de son action. Zone verte jusqu’à 0,1, rouge au-dessus de 0,25.
Google et Yandex prennent en compte les Core Web Vitals dans l’évaluation globale de qualité du site - pas comme facteur de ranking un-pour-un, mais comme signal dans le mix. Un site avec CLS 0,377 perd face aux voisins de la même SERP, même si le contenu et les liens sont plus forts.
À ma première mesure c’était 0,377. Ça veut dire que Lighthouse voyait la page littéralement sauter. Pas étonnant : j’avais priorisé un lancement rapide plutôt que de polir les métriques. Quatre jours après la mise en prod, je m’y suis mis et j’ai fermé les trois causes en une soirée.
Ronde 1 : dimensions du logo en HTML
Première chose remontée par Lighthouse dans Performance Insights : “Image elements do not have explicit width and height”. Concrètement, le logo dans le header. Dans header.php j’avais ça :
<a href="/" class="header__logo">
<img src="/logo.svg" alt="Société" />
</a>
Le navigateur ne sait pas quelle taille aura le logo une fois affiché tant qu’il ne l’a pas vraiment téléchargé. Et tant qu’il ne l’a pas téléchargé - il réserve l’espace par défaut. Quand le logo se charge, sa vraie taille ‘pousse’ les blocs voisins. Le header se décale brutalement d’une demi-seconde, et le hero glisse avec.
Correction élémentaire :
<a href="/" class="header__logo">
<img src="/logo.svg" alt="Société" width="180" height="44" />
</a>
Le navigateur voit les attributs, calcule l’aspect-ratio 180/44, réserve l’espace immédiatement. Quand le logo se charge, aucun décalage.
Même histoire pour l’image hero sur la page d’accueil - <img> sans dimensions et sans réservation CSS. J’y ai aussi ajouté width="1200" height="630" et, pour la sécurité, style="aspect-ratio: 1200/630". Après ces deux corrections, le CLS est tombé de 0,377 à environ 0,21. Zone jaune, mais pas encore verte.
Leçon. Les dimensions sur chaque <img> sont la correction CWV la moins chère. Supportées par tous les navigateurs depuis 2020, aucun risque. Si vous avez un vieux site sans ça - c’est la première chose à mettre, avant tout le reste.
Ronde 2 : header-nav en critical CSS
Après la première correction, le header continuait à ‘pousser’ depuis le haut pendant une demi-seconde. Dans l’onglet Performance de Chrome DevTools, je voyais : entre First Paint et First Contentful Paint le navigateur n’appliquait pas encore les styles .header-nav, donc le header s’affichait comme une colonne de liens - sans flex, sans espacement, sans bordure. Et après, quand le fichier CSS se chargeait, le header prenait brutalement sa forme.
Dans mon <head> il y avait déjà un bloc critical CSS inline - styles body, .hero, h1, typographie de base. Mais pas de règles pour le header. Elles étaient dans /css/main.css, chargé en requête séparée et appliqué après.
J’ai déplacé le bloc header en critical inline :
<style>
/* ...déjà présent... */
/* CRITICAL: header */
.header { background: #fff; border-bottom: 1px solid #e8e8e8; padding: 14px 0; }
.header__inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; gap: 32px; }
.header__logo { display: inline-flex; }
.header-nav { display: flex; gap: 24px; align-items: center; }
.header-nav a { color: #1a1a1a; text-decoration: none; font-weight: 500; }
/* + burger mobile */
@media (max-width: 768px) { .header-nav { display: none; } .header__burger { display: block; } }
</style>
Mêmes règles retirées de main.css pour éviter le doublon. Après la regénération, le CLS est tombé de 0,21 à 0,05. Zone verte.
Idée simple - tout ce qui s’affiche au-dessus de la fold et est visible au premier frame doit vivre dans le critical CSS inline. Le navigateur peint la page en une seule passe, sans repaint.
Ronde 3 : Inter Fallback avec size-adjust
CLS 0,05 c’est déjà bien, mais je voulais le pousser presque à zéro. Le décalage restant venait de la police. Inter, je l’hébergeais moi-même dans /fonts/inter.woff2 et je le chargeais comme ça :
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
body { font-family: 'Inter', Arial, sans-serif; }
Ce qui se passe au chargement. Premier frame - le navigateur voit qu’Inter n’est pas encore là, affiche le texte en Arial. Le texte prend la place selon les métriques d’Arial - largeur des lettres et hauteur de ligne différentes. Quand le woff2 finit de charger (200-400 ms en 4G), le navigateur redessine instantanément le texte en Inter. Les métriques changent : une phrase qui tenait sur deux lignes en prend une et demie. Le bloc en dessous monte de 28 pixels. Le CLS compte ça comme décalage.
La solution : une police de secours avec les mêmes métriques que la cible. J’ai pris Arial comme base (présente partout, rien à télécharger) et je l’ai calée avec size-adjust et ascent-override pour que ses métriques correspondent à Inter.
La combinaison qui marche :
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
body { font-family: 'Inter', 'Inter Fallback', Arial, sans-serif; }
Ce que ça fait. Le navigateur tente Inter en premier - tant qu’il n’est pas là, il descend la stack. Il voit ‘Inter Fallback’ - en gros Arial avec des métriques étirées. Le texte prend la même place qu’Inter prendra après chargement. Quand le vrai Inter arrive 300 ms plus tard, le texte change juste d’apparence mais ne décale pas la mise en page - les métriques sont déjà alignées.
Où trouver les valeurs. Je les ai prises dans le repo Capsize, il y a des tableaux prêts pour les paires ‘police cible + Arial’ populaires. Pour Inter avec Arial : size-adjust:107%, ascent-override:90%, descent-override:22%. Si vous avez une autre police principale, calculez avec Capsize ou fontkit.
Après cette correction le CLS est tombé à 0,002. Lighthouse mobile - zone verte, le champ du rapport est passé au vert. Par sport j’ai aussi passé PageSpeed Insights sur les field data de Chrome - 0,002 aussi, après trois jours d’accumulation.
Ce qui a été mesuré après
| Métrique | Avant | Après |
|---|---|---|
| CLS Lighthouse mobile | 0,377 | 0,002 |
| CLS PageSpeed field data | (n/a, site neuf) | 0,002 (après 3 jours) |
| LCP mobile | 4,5 s | 2,4 s (au passage avec le CLS - grâce au preload hero) |
| Lighthouse Performance | ~60 | 93 |
En parallèle du CLS sur la ronde 3, j’ai ajouté <link rel="preload" as="image" href="/hero.webp" fetchpriority="high"> pour l’image hero - ça a tiré le LCP vers le bas. Pas vraiment partie de l’histoire CLS, mais l’image a suivi.
Si je recommençais - jour un, pas jour quatre
Le bilan. Les trois corrections pouvaient être faites au lancement, avant la mise en prod. Elles ne nécessitent pas de réécrire le site, pas de nouvelle stack, ça tient en 3-4 heures sur deux cafés.
Je les ai repoussées ‘à plus tard’, et le site a vécu en zone rouge pendant quatre jours. Sur un nouveau domaine, ces quatre jours pouvaient me coûter du ranking : Google comme Yandex prennent les CWV dans la première évaluation de qualité d’un site neuf. Si la première mesure est rouge - rattraper après, c’est long.
Checklist jour un pour tout site neuf :
- Toutes les
<img>avec attributswidthetheight. Règle, pas une recommandation. - Header et hero entièrement en critical CSS inline. Aucune requête externe pour les styles au-dessus de la fold.
- Polices auto-hébergées avec un fallback via
size-adjustdepuis la police système. Ne laissez pas Arial sans ajustement - il ne correspond pas en métriques à Inter, Roboto ni Open Sans. - Passe Lighthouse avant la mise en prod. CLS > 0,1 - la prod attend que ce soit fermé.
Le cas complet sur 50 jours de SEO en B2B nettoyage - dans l’article ‘50 jours de SEO en B2B nettoyage’, où le CLS est un des points dans une image plus large des métriques.
À lire aussi : comment je travaille - sans blabla, sans propositions de 40 pages.