Maîtriser les Core Web Vitals avec Next.js : INP sous 100ms et Zéro CLS
Éliminer les décalages de mise en page, optimiser le chargement des polices et la réactivité.
1. Éliminer le CLS avec next/font et les ratios d’aspect
Le CLS mesure les mouvements inattendus de contenu visible — le score est fonction de la portion de viewport qui s’est déplacée et de l’ampleur du déplacement. Il est presque toujours causé par du contenu qui se rend après le premier affichage et repousse tout ce qui suit : une image sans dimensions réservées, une police web qui bascule vers un fallback plus large ou plus haut, un emplacement publicitaire ou un embed qui charge en retard, ou une bannière injectée au-dessus d’un contenu existant. Chacun de ces cas est corrigible, et la correction est mécanique une fois identifiée la cause exacte.
Le cas des images est le plus fréquent et le plus facile à éliminer. Les navigateurs ne peuvent réserver de l’espace pour une image tant qu’ils ne connaissent pas son ratio d’aspect, donc toute balise `<img>` sans `width` et `height` explicites (ou un `aspect-ratio` CSS) provoque un décalage au moment du chargement. Le composant `<Image>` de Next.js exige width et height (ou `fill` avec un parent dimensionné) précisément pour forcer cette réservation, et il devrait être la seule façon dont les images entrent dans la page — une balise `<img>` brute réintroduit exactement le bug que le composant est censé prévenir.
Le basculement de police est la deuxième cause la plus fréquente et la moins évidente. `next/font` résout ce problème en auto-hébergeant les polices et en générant à la compilation une police de fallback aux métriques calquées (`size-adjust`, `ascent-override`), si bien que la police de repli du navigateur occupe la même boîte que la police web finale — le texte est lisible dès le premier affichage, dans une police système, et ne se déplace pas lorsque la vraie police arrive, car les deux sont métriquement compatibles. Charger des polices depuis un CDN via une balise `<link>` classique réintroduit le problème classique de flash de texte non stylé suivi d’un reflow que `next/font` existe pour éliminer.
Le contenu injecté dynamiquement — bannières de cookies, bandeaux promotionnels, publicités à chargement tardif — est la catégorie la plus difficile car ce contenu est souvent légitimement inconnu au moment du rendu. La solution n’est pas de l’éviter, mais de réserver l’espace : donnez au conteneur une `min-height` dimensionnée pour le contenu final, ou positionnez les éléments réellement flottants en `position: fixed` ou `absolute` pour qu’ils se superposent plutôt que de repousser le contenu. Tout ce qui s’insère dans le flux normal du document au-dessus de la ligne de flottaison après le rendu initial est une régression de CLS qui ne demande qu’à être mesurée.
2. Optimiser l’INP (Interaction to Next Paint)
L’INP a remplacé le First Input Delay comme métrique Core Web Vitals de réactivité parce que le FID ne mesurait que le délai avant qu’un gestionnaire d’événement commence à s’exécuter — il ne disait rien sur la durée du gestionnaire lui-même, ni sur le temps que prenait le navigateur pour peindre le résultat. L’INP mesure l’interaction complète : du clic ou de la frappe de l’utilisateur jusqu’à la prochaine frame réellement peinte par le navigateur, en capturant la pire interaction (ou proche de la pire) sur toute la durée de vie de la page, pas seulement la première.
La cause dominante d’un mauvais INP est une tâche longue sur le thread principal — tout JavaScript synchrone qui s’exécute pendant plus d’environ 50ms et empêche le navigateur de répondre aux entrées ou de peindre une frame. Cela inclut les re-rendus coûteux déclenchés par une mise à jour d’état, le travail synchrone à l’intérieur d’un gestionnaire d’événement (trier un grand tableau, exécuter une regex sur une longue chaîne, sérialiser un gros objet), et les scripts tiers qui accaparent le thread principal pendant leur propre exécution.
La solution consiste à sortir le travail non essentiel du chemin critique de l’interaction. `startTransition` marque une mise à jour d’état comme non urgente, permettant à React de l’interrompre pour garder l’interface réactive face à des mises à jour plus urgentes (comme la frappe suivante) au lieu de bloquer sur un re-rendu potentiellement coûteux. Pour un calcul réellement lourd — filtrer un grand jeu de données, parser un gros payload — déplacer le travail vers un Web Worker le retire entièrement du thread principal, si bien qu’il ne peut plus bloquer le rendu ni la gestion des entrées, quelle que soit sa durée.
Un second facteur souvent négligé d’un mauvais INP est le coût de l’hydratation au chargement d’une route : une page qui envoie beaucoup de JavaScript client traverse une fenêtre pendant l’hydratation où les gestionnaires d’événements sont attachés mais le thread principal est occupé par la réconciliation de React, si bien que les toutes premières interactions après la navigation semblent lentes alors que rien n’est techniquement défaillant. C’est là que le modèle « composants serveur par défaut » de l’App Router porte à nouveau ses fruits : moins de JavaScript client à hydrater signifie une fenêtre d’inertie plus courte juste après que la page devient visible.
3. Formats d’images nouvelle génération
Les images restent le principal contributeur au poids de page sur la plupart des sites, et le choix du format à lui seul explique généralement une grande part des économies réalisables. AVIF compresse en général mieux que WebP pour une qualité visuelle équivalente, et les deux sont substantiellement plus légers que JPEG ou PNG à qualité comparable ; la décision pratique n’est presque jamais de choisir le format à la main, mais de s’assurer que votre pipeline sert automatiquement le meilleur format supporté par chaque navigateur.
`next/image` gère cette négociation pour vous : il sert AVIF ou WebP aux navigateurs qui les supportent et retombe sur le format d’origine sinon, génère plusieurs résolutions et laisse le navigateur choisir via `srcset`, et charge paresseusement par défaut toute image sous la ligne de flottaison pour qu’elle ne monopolise pas la bande passante face au contenu déjà visible par l’utilisateur. Rien de tout cela ne nécessite d’éléments `<picture>` manuels ni de configuration séparée de CDN d’images — c’est le comportement par défaut du composant.
Les deux réglages les plus importants en pratique sont `priority` et `sizes`. `priority` doit être défini exactement sur les images visibles sans défilement sur un viewport typique — généralement l’image hero ou la plus grande image au-dessus de la ligne de flottaison — car cela supprime le lazy-loading et ajoute une indication de préchargement, améliorant directement le Largest Contentful Paint. `sizes` indique au navigateur la taille réelle de rendu de l’image à chaque point de rupture, et se tromper (ou l’omettre) fait télécharger au navigateur une image plus grande que nécessaire, source silencieuse fréquente de bande passante gaspillée sur les mises en page responsive.
4. Suivi continu des performances réelles
Les outils de laboratoire comme Lighthouse sont utiles pour détecter des régressions en CI, mais ils mesurent une exécution synthétique unique sur un appareil et un profil réseau fixes — ils ne disent rien de ce que vivent vos utilisateurs réels sur leurs appareils et connexions réels, qui varient énormément. Les données de terrain, collectées depuis de vraies sessions utilisateur via la bibliothèque `web-vitals` et remontées vers un endpoint analytique, sont ce que le score Core Web Vitals de Google utilise réellement, et c’est la seule source de données qui reflète la réalité : une connexion rurale lente, un téléphone Android vieux de cinq ans, un FAI capricieux.
Mettre cela en place consiste à importer `onCLS`, `onINP` et `onLCP` depuis `web-vitals` dans un petit composant client monté une seule fois près du layout racine, et à envoyer chaque métrique à votre backend analytique au fur et à mesure de sa remontée — ces callbacks se déclenchent de façon asynchrone à mesure que les événements pertinents surviennent durant la vie de la page, pas tous en une fois au chargement. Les données sont intrinsèquement bruitées au niveau d’une session individuelle, donc le signal utile est le p75 (75e percentile) sur une fenêtre glissante, qui est aussi le percentile utilisé par le rapport Core Web Vitals de Google pour classer une page comme bonne, à améliorer ou mauvaise.
Une fois les données réelles en circulation, leur usage le plus précieux consiste à segmenter par gabarit de page, type d’appareil et vitesse de connexion plutôt que de regarder une moyenne unique sur tout le site — une page d’accueil et un tunnel de commande ont des profils de vitals complètement différents, et un chiffre unique mélangé masque lequel des deux échoue réellement. Suivre le p75 de l’INP par route dans le temps transforme aussi une plainte subjective du type « le site semble plus lent ces derniers temps » en une régression mesurable que vous pouvez confronter aux déploiements récents.
5. Synthèse : une checklist pour chaque nouvelle page
Les Core Web Vitals ne sont pas trois chiffres indépendants à traquer séparément — ce sont les symptômes d’un petit nombre de causes racines, et corriger ces causes fait bouger les trois à la fois. L’espace non réservé (images, polices, contenu dynamique) alimente le CLS. Le JavaScript à exécution longue sur le thread principal alimente l’INP. Les ressources non optimisées ou surdimensionnées sur le chemin de rendu critique alimentent le LCP. La plupart des corrections sont structurelles, appliquées une fois au niveau du composant ou de la configuration, plutôt qu’un réglage page par page.
Une checklist concrète pour livrer une nouvelle page : chaque image passe par `next/image` avec des dimensions explicites et `priority` réservé uniquement au hero au-dessus de la ligne de flottaison ; chaque police passe par `next/font` plutôt que par un `<link>` de CDN ; tout gestionnaire d’interaction effectuant un travail non trivial enveloppe sa mise à jour d’état dans `startTransition`, ou déplace la partie lourde vers un Web Worker ; tout contenu à chargement tardif (bannières, embeds, publicités) reçoit un conteneur à `min-height` réservée avant son rendu ; et le reporting terrain `web-vitals` est branché dès le premier jour pour que les régressions apparaissent dans des données réelles, pas seulement dans un run Lighthouse découvert trois mois plus tard.

