Mercredi 10h. Tu reçois un mail d’un client qui a fait tourner Lighthouse sur sa landing page produit. Le score est à 42. LCP à 6,8 secondes, CLS à 0,28, INP à 380 ms. Le site est sous Next.js, hébergé chez un cloud provider de premier plan. Le dev team pense que le problème vient du bundle JavaScript. Ils ont déjà coupé 30 % du poids des images, passé le chargement en lazy, et minifié le CSS. Le score a gagné 3 points. Le client veut savoir pourquoi ça ne suffit pas.

L’erreur la plus coûteuse en Core Web Vitals : traiter les trois métriques comme une liste de tâches indépendantes. Optimiser, c’est identifier quel signal bloque les autres et attaquer le goulot d’étranglement, pas cocher une checklist Lighthouse.

Les trois métriques ne sont pas trois problèmes distincts

LCP, INP, CLS : trois signaux, mais pas trois silos. Le LCP mesure quand le plus gros élément du viewport s’affiche, l’INP la réactivité de la page sur l’interaction la plus lente, le CLS l’instabilité visuelle au chargement.

Ces valeurs s’empilent comme des dominos. Un TTFB dégradé repousse le LCP. Un LCP tardif retarde l’instant où le thread principal devient disponible pour les interactions. Un layout qui se recalcule parce qu’une police arrive en retard relance du rendu et dégrade l’INP.

L’ordre d’attaque qui tient : TTFB d’abord, LCP ensuite, INP en troisième, CLS en quatrième. Pas parce que le CLS est secondaire, mais parce qu’il est souvent un effet secondaire des trois premiers.

Le TTFB est ton plancher de LCP, et personne ne le regarde

Quand on parle d’optimisation, l’attention va directement aux images, au lazy-loading, au préchargement. C’est logique : ce sont les leviers visibles dans Lighthouse. Mais aucun d’eux n’a d’impact si le serveur met 1,8 seconde à envoyer le premier octet.

Le TTFB, c’est le temps entre la requête du navigateur et le premier octet de la réponse HTML. Si ton TTFB est à 1200 ms, ton LCP ne descendra jamais sous les 1,8 seconde, même avec un document HTML vide et une image préchargée en fetchpriority="high". La raison est mécanique : l’élément LCP est, dans la grande majorité des pages, une balise <img> ou un bloc de texte dont l’URL est découverte dans le HTML. Le navigateur ne peut pas la découvrir avant d’avoir reçu le HTML. Le LCP ne peut pas commencer avant la fin du TTFB.

Sur le cas d’ouverture, le TTFB pointait à 1400 ms depuis Paris, 2100 ms depuis Lyon. Le site tournait sur une fonction serverless dans une région cloud unique, sans CDN devant le HTML. La correction n’a pas touché une ligne de front-end : déploiement d’un CDN avec edge caching du HTML sur les routes non authentifiées, ajout d’un stale-while-revalidate à 60 secondes. Le TTFB est passé à 85 ms. Le LCP est descendu de 6,8 à 3,2 secondes en une seule mise en production. Aucune optimisation d’image, aucun changement de bundle. Juste le chemin réseau.

Tu mesures le TTFB avec un curl chronométré ou, plus simplement, dans l’onglet Network des DevTools, colonne Waiting. Avant même d’ouvrir un rapport Lighthouse, vérifie cette colonne. Si elle dépasse 300 ms sur un site au trafic majoritairement géolocalisé dans un pays, un audit SEO technique structuré doit commencer par là, pas par une chasse aux images lourdes.

Le LCP est un problème de séquence, rarement de poids d’image

!A hand placing a large picture frame last in a row of smaller frames on a white gallery wall, soft daylight casting gent

Une fois le TTFB maîtrisé, la question devient : qu’est-ce qui empêche l’élément LCP d’apparaître vite ?

Optimiser le LCP, c’est rarement compresser l’image la plus grande. Le vrai problème, c’est la chaîne de dépendances critiques qui amène à cette image. Si elle est chargée par un <img> présent dès le HTML initial, avec fetchpriority="high" et sans lazy-loading, le navigateur la découvre tôt et la priorise. Si elle est injectée par JavaScript après hydratation, il la découvre tard. Si une feuille CSS bloque le rendu avant que l’image ne soit découverte, le LCP recule.

La priorité de chargement ne se devine pas, elle se déclare

Sur le site de notre client, l’image LCP était une balise <img> avec loading="lazy". Pour une image située au-dessus de la ligne de flottaison, c’est un contresens. Le lazy-loading natif demande au navigateur de repousser le chargement d’une ressource. Si cette ressource est l’image principale du viewport, tu viens de repousser ton LCP de plusieurs centaines de millisecondes. Le loading="lazy" n’est pertinent que pour les images en dessous du pli. Supprimé, le LCP a gagné 400 ms.

Ensuite, l’URL de l’image LCP n’était pas déclarée dans un <link rel="preload">. Sans cette balise, le navigateur découvre l’image en parsant le HTML, puis en la rencontrant dans le DOM. À ce stade, d’autres ressources (CSS, polices, scripts bloquants) sont peut-être déjà en cours de chargement et monopolisent les connexions HTTP prioritaires. Un preload explicite avec as="image" et fetchpriority="high" donne au navigateur l’instruction sans ambiguïté : charge cette image en premier, avant le CSS, avant les fonts, avant le logo dans le header.

<link rel="preload" as="image" href="/produit-hero.webp" fetchpriority="high">

Cette ligne, placée dans le <head>, vaut plus que 50 % de compression supplémentaire sur une image déjà au format WebP.

Le SSR ne suffit pas si le HTML cache le signal

Un autre pattern qu’on voit souvent : le HTML est servi par le serveur (SSR), l’image est dans le HTML, et pourtant le LCP est médiocre. La raison tient parfois à une redirection JavaScript. Certains lazy-loaders basés sur l’Intersection Observer retirent l’attribut src de l’image pour le remplacer par data-src, même quand l’image est dans le viewport immédiat. Résultat : le navigateur parse le HTML, voit une balise sans src, ne peut pas la charger, attend le JavaScript, exécute l’observer, injecte le src, et seulement là commence le chargement de l’image LCP. Le délai entre le parsing HTML et l’injection du src peut atteindre 600 ms sur un mobile milieu de gamme.

La correction est simple : ne jamais lazy-loader l’image candidate au LCP. Sur les frameworks qui lazy-load tout par défaut, on isole le composant et on désactive le mécanisme pour lui. Sale architecturalement, mais c’est ce que mesure Googlebot.

L’INP frappe aussi les sites peu interactifs

L’INP a officiellement remplacé le FID comme signal Core Web Vital en mars 2024. La différence est fondamentale. Le FID mesurait le délai entre la première interaction et le début du traitement. L’INP mesure le délai entre l’interaction et le prochain affichage visuel, et ce n’est pas la première interaction qui compte, c’est la plus lente parmi toutes les interactions de la session. Une seule interaction qui traîne fait plonger le score, même si les 99 autres répondent en 40 ms.

Le cas le plus pervers, c’est le site e-commerce qui n’a presque pas d’interactivité lourde. Pas de chat en direct, pas de carte interactive, pas de filtres en temps réel. Juste un clic sur « Ajouter au panier ». Sur le papier, l’INP devrait être excellent. Dans la réalité, le clic tombe 800 ms après le chargement de la page, pile au moment où un script d’analytics injecte un iframe de suivi de conversion dans le DOM. Le thread principal est occupé à parser et exécuter ce script tiers. Le clic est mis en file d’attente. Résultat : 320 ms d’INP.

Découper pour ne pas bloquer le thread

Le problème n’est pas la quantité totale de JavaScript, c’est la longueur des tâches qui monopolisent le thread. Une tâche unique de plus de 50 ms est une « long task ». Au-delà de 100 ms, l’utilisateur perçoit un délai.

Le remède, c’est le découpage. Une fonction d’initialisation qui parse un JSON de 50 Ko, hydrate un state manager et monte un composant de consentement cookie doit être scindée. La première partie traite le JSON critique en 20 ms. La seconde planifie le reste via setTimeout avec un délai de 0, ce qui remet la tâche en file d’attente et laisse le thread respirer entre les blocs.

setTimeout(() => {
  initNonCriticalUI();
}, 0);

C’est un yield manuel. Pas élégant, mais efficace. Les navigateurs modernes proposent scheduler.postTask() avec priorisation, une API plus fine, mais le principe reste le même : ne pas bourrer le thread sans pause.

Les frameworks récents apportent leur propre réponse. Les React Server Components réduisent la quantité de JavaScript envoyé au client, ce qui diminue mécaniquement le temps d’hydratation initial et libère le thread plus tôt pour les interactions. Sur un site e-commerce, passer les fiches produits en Server Components peut réduire le bundle client de 30 à 50 %, ce qui se traduit directement sur l’INP.

Le CLS est un bug de layout, pas un problème de performance réseau

!A crooked picture frame hanging on a wall with a loose wire, misaligned with a row of straight frames, soft afternoon li

Le Cumulative Layout Shift est le plus traître des trois signaux parce qu’il est souvent invisible en développement. Tu charges la page en localhost, toutes les ressources sont en cache, les polices sont installées sur ta machine, les pubs et les bannières de consentement ne se déclenchent pas. Le score CLS est à 0. Une fois en production, une police web met 700 ms à arriver, le texte bascule, le bouton « Commander » se déplace sous le pouce de l’utilisateur au moment où il allait cliquer. Le CLS grimpe à 0,25.

L’essentiel des CLS qu’on rencontre vient de trois sources, dans l’ordre :

  1. Des images sans dimensions explicites injectées dans un flux responsive.
  2. Des polices web chargées sans font-display: swap avec une fallback mal dimensionnée.
  3. Du contenu dynamique (bannières, formulaires de newsletter) inséré au-dessus du contenu existant sans réservation d’espace préalable.

Les images sans ratio d’aspect sont la cause n°1 de CLS

Une balise <img> sans attributs width et height explicites, c’est un rectangle de taille zéro dans le layout initial. Le navigateur calcule la mise en page sans savoir quelle place réserver. L’image arrive, ses dimensions réelles sont connues, le layout se recalcule, tout ce qui est en dessous se décale. C’est le mécanisme de base du CLS.

Depuis que les navigateurs supportent le ratio d’aspect natif à partir de width et height en HTML (même combinés avec max-width: 100% en CSS pour le responsive), la correction est triviale :

<img src="produit.webp" width="800" height="600" alt="Produit" style="max-width: 100%; height: auto;">

Le navigateur réserve une boîte de ratio 4:3 avant même que l’image ne soit chargée. Le layout est stable dès le premier rendu. Sur un CMS, l’injection des dimensions doit se faire à l’upload, pas à la main. C’est moins glamour qu’une optimisation de bundle, mais corriger le score CLS mobile passe par là dans la plupart des cas.

Les polices web : le swap, pas le blocage

Le font-display: swap ne suffit pas si la police de secours n’a pas les mêmes métriques que la police web. Si ton fallback est Arial et ta police web est une condensed étroite, le texte va occuper deux fois plus de largeur au chargement, puis se contracter quand la police web arrive. Le CLS est garanti.

La parade consiste à utiliser size-adjust en @font-face pour aligner les métriques de la fallback sur celles de la police personnalisée. C’est fastidieux mais l’outillage existe : Fontaine, un outil en ligne de commande, calcule les métriques automatiquement. L’investissement est rentable sur une demi-journée de travail.

Monitorer ne signifie pas relancer Lighthouse une fois par semaine

Les Core Web Vitals sont un état, pas une étape. Chaque nouveau bundle, chaque script tiers décale la mesure. La Search Console donne un historique INP et LCP sur 90 jours : c’est le tableau de bord de premier niveau, plus fiable qu’un Lighthouse hebdo lancé à la main. Sur les sites à fort volume d’URL, segmenter par template (meilleur outil de test de vitesse en banc récurrent) et piloter par échantillon évite de courir derrière chaque page. Optimiser le crawl budget sur un grand site passe aussi par là : un site rapide se crawle plus profond sur la même fenêtre.

Questions fréquentes

Quand faut-il vraiment commencer à s’inquiéter de son INP ?

Le seuil « bon » est à 200 ms ou moins. Mais la vraie alerte, c’est quand plus de 25 % des sessions dans la Search Console dépassent 300 ms. C’est là que Google considère le signal comme dégradé. En dessous, l’énergie est mieux investie sur le LCP ou le TTFB, sauf si le site est massivement interactif.

Quelle est la différence concrète entre FID et INP ?

Le FID ne regardait que la première interaction, et seulement son délai avant traitement. L’INP couvre toutes les interactions de la session et prend en compte le temps jusqu’au rendu visuel suivant. Une seule interaction lente sur une session fait plonger le score. Passer de FID à INP, c’est passer d’une photo à un film.

Comment un site WordPress peut-il s’attaquer au LCP sans refonte ?

La priorité est le cache HTML pleine page, servi par un CDN. Ensuite, définir manuellement l’image candidate au LCP et la précharger via le hook wp_head. Pour améliorer le LCP sur WordPress, un plugin de cache bien configuré et une règle de priorisation sur l’image héros couvrent 70 % du chemin. Le reste dépend du thème et des scripts tiers.

Est-ce que Google classe mieux les pages avec un bon score Core Web Vitals ?

Google ne l’exprime pas comme un bonus, mais comme un signal de classement parmi d’autres, avec un seuil plus qu’une gradation infinie. Passer de « médiocre » à « bon » sur les trois métriques donne un avantage mesurable dans les SERP mobiles, surtout sur les requêtes concurrentielles. Passer de « bon » à « excellent » n’apporte pas de gain supplémentaire démontré. L’enjeu réel, c’est la conversion : une page qui charge en 1,2 seconde convertit mieux que la même page à 3 secondes, indépendamment du classement Google.

Quiz personnalisé

Votre recommandation sur optimisation core web vitals

Quelques questions rapides pour adapter la recommandation à votre cas.

Q1 Votre situation sur optimisation core web vitals ?
Q2 Votre priorité ?
Q3 Votre horizon ?