Sirocco

Core Web Vitals 2025 : recettes pour 95+ Lighthouse

INP remplace FID, LCP hero image, fonts self-hosted, Server Components, PPR, CLS cookie banner fix.

4 min de lecture
Core Web Vitals 2025 : recettes pour 95+ Lighthouse

Google a fait évoluer les Core Web Vitals en mars 2024 en remplaçant FID (First Input Delay) par INP (Interaction to Next Paint), et 2025 est la première année complète où les sites sont jugés sur ce nouveau triptyque LCP / INP / CLS. La différence n'est pas cosmétique : FID ne mesurait que le délai de la première interaction, alors qu'INP mesure le pire délai d'interaction sur toute la session. Beaucoup de sites qui affichaient 100/100 Lighthouse sous FID se retrouvent à 60 sous INP, parce que leurs handlers `onChange` ou leurs scrolls coûtent en réalité 400 ms à 800 ms après quelques secondes d'usage. Cet article décrit les recettes concrètes pour franchir 95+ en Lighthouse et — plus important — pour rester verts sur les Field Data de la Search Console. Le LCP (Largest Contentful Paint) reste la métrique la plus directement actionnable. Le candidat LCP est presque toujours soit le hero image, soit le titre H1 si la page n'a pas d'image dominante. La recette éprouvée tient en quatre points : servir l'image en AVIF (avec fallback WebP via `<picture>`), la dimensionner au breakpoint réel sans upscaling, la déclarer `fetchpriority="high"` et la précharger via `<link rel="preload" as="image" imagesrcset="...">` dans le `<head>`. Sur une connexion 4G simulée à Slow 3G, ces quatre changements font passer un LCP de 4,8 secondes à 1,3 seconde sans toucher au reste du HTML. Le deuxième levier LCP est moins évident : la stratégie des fonts. Une font Google chargée via `@import` bloque le rendu du texte pendant 200 à 400 ms en 3G. La parade éprouvée combine self-hosting (woff2 dans le repo, servi via le CDN du site), `font-display: optional` (pas `swap`, parce que swap provoque du CLS quand la font web prend la place de la fallback), subset latin-only si le site n'est pas multilingue, et preload du fichier woff2 critique dans le `<head>`. Sur Next.js, `next/font/local` automatise tout ça, à condition de désactiver `display: swap` et de spécifier `adjustFontFallback` correctement. L'INP est la nouvelle métrique qui pénalise les architectures legacy à fort JavaScript client. Le principe à retenir : INP mesure le temps entre une interaction utilisateur (click, tap, keypress) et le prochain frame peint. Tout handler synchrone qui prend plus de 200 ms — un setState global qui re-render un arbre React de 500 composants, une recherche client sur une liste de 10 000 items, un filter sur un Redux store volumineux — détruit le score. La parade structurelle est de migrer vers Server Components partout où c'est possible, ce qui sort des dizaines de KB de bundle client, et de wrapper les actions coûteuses dans `useTransition` ou `useDeferredValue` pour les marquer comme interruptibles côté React 18+. Pour les zones qui doivent rester client, deux techniques bouclent l'INP. Premièrement, `requestIdleCallback` (ou `scheduler.postTask` quand disponible) pour les calculs non-critiques, ce qui repousse le travail à un moment où le thread principal est libre. Deuxièmement, le découpage des longues tâches : un tri sur 10 000 items doit être chunked en batches de 50 ou 100 avec `setTimeout(0)` entre les batches, ou mieux, déplacé dans un Web Worker. La règle pratique : aucune tâche du main thread ne devrait excéder 50 ms ; au-delà, le navigateur classe la tâche en « long task » et l'INP s'en ressent. Le CLS (Cumulative Layout Shift) est apparemment résolu sur la plupart des sites modernes, mais il reste deux pièges fréquents. Le cookie banner injecté en JavaScript après le premier paint pousse tout le contenu vers le bas, ce qui produit un CLS catastrophique sur la première interaction. La parade est de réserver la hauteur du banner directement dans le HTML server-rendered (un `<div style="height: 80px">` qui devient le banner après hydratation). Le second piège est l'image sans `aspect-ratio` : sans cette propriété, le navigateur ne connaît pas la taille de l'image avant qu'elle ne charge et compense par un saut de layout. La règle est simple : toute image et toute iframe doit avoir un `width`/`height` ou un `aspect-ratio` CSS. Next.js 14 avec App Router pose les bons défauts pour ces trois métriques. Server Components réduisent automatiquement le JS client, le composant `<Image>` gère l'AVIF/WebP, les dimensions et le preload automatique du LCP. Une fonctionnalité moins connue mais puissante est PPR (Partial Prerendering, encore en flag stable en 2025) : la coquille statique est servie depuis l'Edge en sub-100 ms, et seuls les fragments dynamiques (prix temps réel, panier, personnalisation) sont streamés. C'est ce qui permet de garder un LCP < 1,5 seconde même sur des pages qui contiennent du contenu personnalisé. La boucle de mesure compte autant que les optimisations. Lighthouse en CI donne la vérité synthétique sur un environnement contrôlé, mais c'est la Field Data de la Search Console qui décide du ranking. Brancher un Real User Monitoring (Vercel Speed Insights, SpeedCurve, ou simplement le hook `useReportWebVitals` de Next.js qui envoie les métriques vers son endpoint) permet de croiser synthétique et réel. Un site qui passe de Lighthouse 38 à 96 en deux sprints, c'est faisable et nous l'avons vu plusieurs fois ; ce qui demande de l'expérience, c'est de maintenir 96 quand les équipes produit poussent des sliders et des tracking pixels chaque semaine.