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 width et height sur le logo dans le header. ~40 % du décalage gagné.
  • Correction n°2 : règles de .header-nav dé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% et ascent-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étriqueAvantAprès
CLS Lighthouse mobile0,3770,002
CLS PageSpeed field data(n/a, site neuf)0,002 (après 3 jours)
LCP mobile4,5 s2,4 s (au passage avec le CLS - grâce au preload hero)
Lighthouse Performance~6093

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 attributs width et height. 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-adjust depuis 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.

Questions fréquentes

Qu'est-ce que le CLS et quelle valeur est normale ?
Cumulative Layout Shift c'est la somme de tous les décalages de mise en page inattendus pendant le chargement. Zone verte jusqu'à 0,1, jaune 0,1-0,25, rouge au-dessus de 0,25. Google et Yandex utilisent le CLS comme facteur de ranking depuis 2021. Sur mon site c'était 0,377 - Lighthouse considérait que la moitié de l'écran sautait pendant le chargement.
Pourquoi la police de secours affecte-t-elle le CLS ?
Pendant que le navigateur télécharge le woff2 d'Inter, le texte s'affiche en police de secours (Arial ou system-ui). Quand Inter se charge, les métriques changent : largeur des lettres, hauteur de ligne. Le texte 'se refait', les blocs en dessous glissent. C'est le FOUT (Flash of Unstyled Text) - à 1-2 secondes de chargement sur mobile, ça produit un énorme décalage.
C'est quoi size-adjust dans @font-face et comment le calculer ?
C'est une propriété CSS qui met à l'échelle la police de secours pour faire correspondre les métriques à la police cible. Le calcul : on prend la x-height et les largeurs moyennes des caractères de la cible et du fallback, puis on choisit un size-adjust (80% à 120%) tel que le texte occupe le même espace. Pour la paire Inter ↔ Arial, la valeur qui marche est 107% avec un ascent-override:90% supplémentaire. On peut utiliser le calculateur fontkit ou prendre les valeurs du repo Capsize.
Critical CSS - c'est quoi et pourquoi y mettre le header ?
Critical CSS, ce sont les styles nécessaires pour afficher la partie visible de la page (au-dessus de la fold). On les inline directement dans `<head>` pour que le navigateur n'attende pas un fichier CSS externe avant le premier rendu. Si les styles du header sont dans un fichier séparé, le navigateur affiche d'abord le contenu sans header, puis charge le CSS, et le header 'saute' depuis le haut. Ça donne déjà un décalage de 0,05-0,15. Le déplacer dans critical élimine cette étape.
Ça marche sur un CMS, ou seulement en code maison ?
L'approche est universelle. Sur WordPress il y a des plugins comme Critical CSS by NitroPack ou Autoptimize qui font ça automatiquement. Sur Tilda et constructeurs similaires on ne peut pas toucher au critical CSS sans exporter en code. Sur Astro / Next.js c'est intégré au bundler. Les dimensions du logo en HTML marchent partout aussi : les attributs `width` et `height` sur `<img>` sont supportés par tous les navigateurs depuis 2020.