Photo : Andrea Piacquadio · PexelsCore Web Vitals 2026 : optimiser un site Next.js pour les scores parfaits
INP remplace FID, les seuils ont évolué — voici comment diagnostiquer et corriger LCP, INP et CLS sur un projet Next.js avec des méthodes éprouvées sur des projets B2B.
Nos clients types
#Core Web Vitals en 2026 : ce qui a changé
Depuis mai 2023, Google a officiellement remplacé le First Input Delay (FID) par l'Interaction to Next Paint (INP) dans les signaux de ranking. En 2026, ce changement est pleinement intégré dans Google Search Console, PageSpeed Insights et le rapport CWV du Chrome User Experience Report (CrUX).
Les trois métriques officielles et leurs seuils 2026 :
| Métrique | Bon | Amélioration nécessaire | Mauvais |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2,5 s | 2,5 – 4,0 s | > 4,0 s |
| INP (Interaction to Next Paint) | ≤ 200 ms | 200 – 500 ms | > 500 ms |
| CLS (Cumulative Layout Shift) | ≤ 0,1 | 0,1 – 0,25 | > 0,25 |
Google ne publie pas de formule exacte sur le poids de chaque métrique dans le ranking, mais la documentation officielle de Search Central confirme qu'un site dans la zone « Bon » sur les trois métriques bénéficie d'un signal positif dans le classement mobile et desktop.
Une part importante des sites web ne passe pas les seuils « Bon » sur les trois métriques simultanément (voir le HTTP Archive Almanac). Sur les sites Next.js bien configurés, ce taux est nettement meilleur : l'écart s'explique par des choix d'architecture, pas par la taille des projets.
#LCP — Largest Contentful Paint : diagnostic et 8 optimisations Next.js
#Comprendre ce que mesure le LCP
Le LCP mesure le temps nécessaire pour que l'élément de plus grande surface visible dans le viewport soit rendu. Sur la majorité des pages, cet élément est soit une image hero, soit un bloc de texte H1 large. Dans Google Search Console, le rapport « Signaux web essentiels » indique quel élément constitue le LCP de vos URLs les plus visitées.
#Diagnostic LCP en pratique
Avant de corriger, identifiez l'élément LCP réel :
// Dans Chrome DevTools > Performance > filmstrip
// ou en JavaScript pour le debugging
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP element:', entry.element)
console.log('LCP time:', entry.startTime)
}
}).observe({ type: 'largest-contentful-paint', buffered: true })
#Optimisation 1 — Composant Image avec priority
Le composant <Image> de Next.js ajoute automatiquement loading="lazy" sur toutes les images. Pour l'image LCP, cette valeur doit absolument être contournée :
import Image from 'next/image'
// Image hero = candidate LCP → priority obligatoire
<Image
src="/hero-produit.webp"
alt="Interface de gestion B2B — dashboard Nehos"
width={1200}
height={630}
priority // ← génère fetchpriority="high" + preload dans le <head>
quality={85}
/>
Sans priority, l'image est chargée en lazy — elle n'est même pas demandée au réseau avant que le navigateur ait fini de parser le HTML. C'est la cause n°1 de LCP dégradé sur les projets Next.js.
#Optimisation 2 — Preload explicite dans le <head>
Pour les images servies depuis un CDN externe (Cloudinary, Imagekit, etc.), Next.js ne génère pas automatiquement le preload. Ajoutez-le manuellement dans votre layout :
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<link
rel="preload"
as="image"
href="https://cdn.nehos-groupe.com/hero-home.webp"
type="image/webp"
/>
</head>
<body>{children}</body>
</html>
)
}
#Optimisation 3 — CDN edge avec cache-control long
Le TTFB (Time to First Byte) impacte directement le LCP. Sur Vercel, les routes statiques et PPR sont automatiquement servies depuis le CDN edge. Sur une infrastructure personnalisée, configurez des headers de cache longs sur les assets statiques :
// next.config.ts
const config: NextConfig = {
async headers() {
return [
{
source: '/_next/static/(.*)',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }
]
}
]
}
}
#Optimisation 4 — Fonts : display: swap et préchargement
Une police web non préchargée bloque le rendu du texte — si le texte est l'élément LCP, le score s'effondre. Avec next/font :
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap', // évite le FOIT (Flash of Invisible Text)
preload: true, // génère le lien preload dans <head>
variable: '--font-inter'
})
#Optimisation 5 — Partial Prerendering pour le shell statique
Le PPR permet de servir le shell de la page (header, hero, navigation) depuis le CDN très rapidement et de streamer les parties dynamiques ensuite. L'élément LCP fait partie du shell statique et est donc visible très tôt :
// app/produits/[slug]/page.tsx
import { Suspense } from 'react'
export default function ProductPage() {
return (
<main>
{/* Servi depuis CDN — l'image hero est dans ce shell */}
<ProductHero />
{/* Streamé depuis le serveur — n'impacte pas le LCP */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews />
</Suspense>
</main>
)
}
#Optimisation 6 — Formats AVIF/WebP automatiques
Next.js 16 négocie automatiquement le format optimal. L'AVIF offre une compression nettement meilleure que le WebP et le JPEG, ce qui raccourcit le téléchargement de l'image hero sur une connexion mobile.
#Optimisation 7 — Éliminer les chaînes de redirections
Chaque redirection ajoute un aller-retour réseau complet. Sur les sites qui empilent des redirections (HTTP → HTTPS, www → non-www, trailing slash), le TTFB effectif peut doubler. Vérifiez avec :
curl -I -L https://monsite.fr/page | grep -E '^(HTTP|Location)'
#Optimisation 8 — SSR sélectif sur les composants critiques
Les composants au-dessus de la ligne de flottaison doivent être rendus côté serveur. Évitez le dynamic(() => import(...), { ssr: false }) sur tout composant qui fait partie du LCP — cela retarde le rendu initial au moment où JavaScript s'exécute côté client.
#INP — Interaction to Next Paint : diagnostic et optimisations
#Ce que mesure l'INP
L'INP mesure la latence entre une interaction utilisateur (clic, frappe clavier, tap) et le moment où le navigateur peint la réponse visuelle. Contrairement au FID qui mesurait uniquement la première interaction, l'INP mesure le pire centile des interactions sur toute la durée de session.
Les causes principales d'INP élevé sur les apps React/Next.js :
- Long Tasks JavaScript sur le thread principal (> 50 ms)
- Rendu de listes longues sans virtualisation
- Calculs synchrones déclenchés par un event handler
- Hydratation complète de pages lourdes au premier clic
#Diagnostic INP
import { onINP } from 'web-vitals'
onINP(({ value, rating, attribution }) => {
console.log('INP:', value, 'ms — rating:', rating)
console.log('Element:', attribution.interactionTarget)
console.log('Long Task duration:', attribution.inputDelay)
})
#Code splitting par route
Next.js 16 split automatiquement par route avec l'App Router. Assurez-vous de ne pas importer de librairies lourdes dans le layout racine — elles seraient chargées sur chaque page :
// app/dashboard/chart/page.tsx
// Import dynamique : Chart.js ne charge que sur cette route
import dynamic from 'next/dynamic'
const BarChart = dynamic(() => import('@/components/BarChart'), { ssr: false })
#Web Workers pour les calculs lourds
Déplacer les calculs coûteux hors du thread principal est la seule façon d'éviter les Long Tasks qui bloquent les interactions :
// lib/heavy-calc.worker.ts
self.onmessage = (event) => {
const result = expensiveDataTransformation(event.data)
self.postMessage(result)
}
// Dans le composant React
const worker = new Worker(new URL('../lib/heavy-calc.worker', import.meta.url))
worker.postMessage(rawData)
worker.onmessage = (e) => setProcessedData(e.data)
#React 19 Transitions pour les mises à jour non urgentes
React 19 stabilise le pattern useTransition pour différer les mises à jour d'état coûteuses. Le navigateur reste réactif aux autres interactions pendant que React recalcule :
import { useTransition } from 'react'
function SearchInput() {
const [isPending, startTransition] = useTransition()
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
function handleChange(e) {
const val = e.target.value
setQuery(val) // mise à jour urgente — input reste réactif
startTransition(() => {
setResults(filterProducts(val)) // non urgent — peut être interrompu
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList data={results} />
</>
)
}
#View Transitions API pour les navigations fluides
Next.js 16 expose un hook useViewTransition qui s'appuie sur la View Transitions API du navigateur. Les navigations entre pages produisent une animation native sans JavaScript côté client — ce qui maintient l'INP bas pendant les transitions.
#CLS — Cumulative Layout Shift : causes et corrections
#Les 4 causes principales de CLS sur Next.js
1. Images sans dimensions déclarées — Sans width et height sur le composant Image, le navigateur ne réserve pas l'espace avant le chargement. L'image s'insère et décale tout le contenu en dessous.
2. Fonts Web avec display: auto ou block — Le chargement asynchrone de la police provoque un flash de mise en page (FOUT / FOIT) qui décale les lignes de texte.
3. Publicités, embeds et iframes sans conteneur de taille fixe — Un bloc publicitaire qui apparaît au-dessus du contenu est la source de CLS la plus fréquente sur les sites éditoriaux.
4. Contenu injecté dynamiquement au-dessus de la ligne de flottaison — Les bannières cookies, les notifications push et les bandeaux promotionnels déclenchent systématiquement du CLS s'ils ne sont pas réservés en amont.
#Correction 1 — aspect-ratio CSS pour toutes les images
/* globals.css */
img, video {
max-width: 100%;
height: auto; /* maintient le ratio et évite le CLS */
}
.card-thumbnail {
aspect-ratio: 16 / 9; /* réserve l'espace avant le chargement */
overflow: hidden;
}
#Correction 2 — Skeleton Loaders pour les blocs dynamiques
Plutôt qu'un simple spinner, un skeleton loader réserve l'espace exact que le contenu final occupera :
// components/ArticleCardSkeleton.tsx
export function ArticleCardSkeleton() {
return (
<div className="animate-pulse">
<div className="aspect-video bg-gray-200 rounded-lg" />
<div className="mt-3 h-5 bg-gray-200 rounded w-3/4" />
<div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
</div>
)
}
// Utilisation avec Suspense
<Suspense fallback={<ArticleCardSkeleton />}>
<ArticleCard slug={slug} />
</Suspense>
#Correction 3 — font-display: optional pour les fonts critiques
Avec optional, si la police n'est pas disponible dans les 100 ms, le navigateur utilise la police de fallback pour ce premier rendu — sans décalage. La police custom sera utilisée à partir du rechargement suivant (depuis le cache) :
const inter = Inter({
subsets: ['latin'],
display: 'optional', // Pas de swap, pas de FOUT → CLS = 0
})
#Correction 4 — Réserver l'espace pour les bandeaux consentement
/* Réserve l'espace du bandeau en haut de page dès le premier rendu */
body {
padding-top: var(--consent-banner-height, 0);
}
/* Défini côté serveur selon l'état du consentement */
:root {
--consent-banner-height: 72px;
}
#Outils de mesure Core Web Vitals en 2026
#1. Google Search Console — rapport Signaux Web Essentiels
L'outil de référence pour les données de terrain (CrUX). Il agrège les mesures réelles de vos visiteurs Chrome sur 28 jours glissants et ventile les URLs par statut (Bon / Amélioration nécessaire / Mauvais). C'est la seule source de données qui impacte directement le ranking.
#2. PageSpeed Insights
PageSpeed Insights combine données de terrain CrUX et données de laboratoire Lighthouse. Utilisez l'API PSI pour l'automatisation :
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed\
?url=https://monsite.fr&strategy=mobile&key=API_KEY" \
| jq '.loadingExperience.metrics'
#3. Chrome DevTools — panneau Performance
Le panneau Performance de DevTools permet d'identifier précisément les Long Tasks, le moment où chaque ressource se charge et quel élément constitue le LCP. L'onglet « Insights » introduit en 2025 résume les problèmes détectés en langage naturel.
#4. Lighthouse CI
Lighthouse CI automatise les audits à chaque déploiement. Configuration minimale dans GitHub Actions :
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push, pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Build Next.js
run: npm run build
- name: Start server
run: npm run start &
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:3000/
http://localhost:3000/blog
http://localhost:3000/services/refonte-web
budgetPath: ./budget.json
uploadArtifacts: true
temporaryPublicStorage: true
// budget.json — seuils par défaut Nehos
[
{
"path": "/*",
"resourceSizes": [
{ "resourceType": "total", "budget": 400 },
{ "resourceType": "script", "budget": 150 },
{ "resourceType": "image", "budget": 200 }
],
"timings": [
{ "metric": "interactive", "budget": 3000 },
{ "metric": "first-contentful-paint", "budget": 1500 },
{ "metric": "largest-contentful-paint", "budget": 2500 }
]
}
]
#5. web-vitals.js — Real User Monitoring
La librairie officielle Google pour capturer les métriques CWV en production, directement depuis les navigateurs de vos vrais utilisateurs :
// app/providers/web-vitals.ts
'use client'
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals/attribution'
function sendToGA4(metric) {
window.gtag('event', metric.name, {
event_category: 'Web Vitals',
event_label: metric.id,
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
non_interaction: true,
metric_rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
metric_delta: metric.delta,
metric_element: metric.attribution?.largestShiftTarget // Pour le CLS
})
}
onCLS(sendToGA4)
onINP(sendToGA4)
onLCP(sendToGA4)
onFCP(sendToGA4)
onTTFB(sendToGA4)
Dans GA4, créez une exploration personnalisée avec les dimensions event_label et metric_rating pour visualiser la distribution de vos métriques CWV par page, par device et par segment d'utilisateurs.
#Budgets de performance : définir et faire respecter les seuils
Un budget de performance est un contrat d'équipe : toute régression qui dépasse le seuil doit être corrigée avant merge. Sans budget, les scores LCP et CLS dégradent progressivement à chaque sprint.

#Approche Nehos sur les projets B2B
Nous définissons trois niveaux de seuils dans budget.json :
- Seuil cible : la valeur que le projet doit atteindre en production
- Seuil warning : dépassé → flag dans la PR, correction recommandée
- Seuil bloquant : dépassé → la PR ne peut pas merger
// budget.json — niveau 3 : bloquant
{
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "total-blocking-time", "budget": 200 }
],
"resourceSizes": [
{ "resourceType": "script", "budget": 175 }
]
}
La règle que nous appliquons systématiquement : le budget est défini dès la phase de découverte, avant la première ligne de code. Le rattraper après coup coûte beaucoup plus cher.
#Exemples de situation : 3 profils de sites B2B
Ces trois exemples sont des situations types, construites pour illustrer la démarche. Ils ne décrivent pas des clients réels et ne comportent aucun résultat chiffré.
#Exemple 1 : SaaS de gestion documentaire (B2B, nombreuses pages)
Situation : un LCP dégradé, causé par une image hero chargée sans priority depuis un CDN externe non préchargé.
Corrections possibles : ajout de priority sur l'image hero, preload explicite dans le <head>, migration vers le composant Image avec AVIF automatique, activation du PPR sur les pages produit.
On mesure l'effet sur les trois métriques après quelques semaines de données de terrain, dans la Search Console.
#Exemple 2 : e-commerce headless B2B (Shopify + Next.js, catalogue volumineux)
Situation : un INP élevé, venant d'un filtre de catalogue qui recalcule tous les produits sur le thread principal à chaque frappe.
Corrections possibles : migration du filtrage vers un Web Worker, virtualisation de la liste avec react-window, useTransition sur la mise à jour des résultats, skeleton loaders sur les tuiles produit.
On mesure l'effet sur l'INP, puis sur la conversion mobile dans GA4 une fois passé dans la zone « Bon ».
#Exemple 3 : site corporate B2B international (plusieurs langues, Next.js App Router)
Situation : un CLS entièrement dû aux fonts Google Fonts chargées en display: swap sans dimensionnement des fallback fonts.
Corrections possibles : migration vers next/font avec display: optional sur les fonts critiques et size-adjust CSS sur les fonts de fallback pour aligner les métriques typographiques, réservation d'espace pour le bandeau consentement.
On mesure l'effet sur le CLS et sur le score Lighthouse mobile.
#Impact sur le ranking SEO : ce que Google confirme en 2026
La documentation Search Central de Google confirme trois éléments :

1. Les Core Web Vitals sont un facteur de ranking actif, mais pas déterminant à eux seuls. Google précise que deux pages de « qualité similaire » verront la page CWV meilleure être favorisée — la qualité éditoriale reste prépondérante.
2. Le seuil de discrimination est le passage de « Amélioration nécessaire » à « Bon ». Passer de « Mauvais » à « Amélioration nécessaire » produit moins d'effet sur le ranking qu'atteindre « Bon ».
3. L'expérience mobile est évaluée indépendamment de l'expérience desktop. Depuis l'index mobile-first, ce sont les scores mobiles qui déterminent le ranking — y compris pour les requêtes desktop.
L'effet des Core Web Vitals sur les positions est difficile à isoler des autres variables : mesurez-le sur vos propres pages dans la Search Console, avant et après correction.
Photos : Tsvetoslav Hristov, https://kaboompics.com/ sur Pexels.
Questions & réponses
Questions fréquentes sur les Core Web Vitals Next.js 2026
Le FID (First Input Delay) mesurait uniquement la latence de la toute première interaction utilisateur sur la page. L'INP (Interaction to Next Paint), qui l'a remplacé définitivement en mai 2023, mesure le pire centile de toutes les interactions tout au long de la session : clics, frappes clavier, taps. L'INP est beaucoup plus représentatif de l'expérience réelle car une page peut très bien répondre rapidement à la première interaction puis ralentir après hydratation.
Lighthouse mesure en conditions de laboratoire contrôlées (connexion simulée, machine sans extensions). Google Search Console agrège les données CrUX — mesures réelles des vrais utilisateurs Chrome sur vos pages. Les causes d'écart les plus fréquentes : des utilisateurs sur réseau mobile lent, des scripts tiers (chat, analytics) qui chargent en production mais pas en lab, ou une personnalisation serveur qui ajoute de la latence pour certains segments d'utilisateurs. Priorisez toujours les données CrUX pour le diagnostic SEO.
Le composant Image résout automatiquement le format (AVIF/WebP), le lazy loading et le srcset responsive. Mais il ne suffit pas seul : vous devez ajouter priority sur l'image candidate LCP, configurer un CDN edge pour réduire le TTFB, et éviter les redirections en chaîne avant la page. La combinaison Image priority + PPR + CDN edge aide fortement à atteindre un bon LCP pour les utilisateurs européens.
Trois outils complémentaires : le panneau Performance de Chrome DevTools montre les Long Tasks (blocs rouges > 50 ms sur la flamechart), la librairie web-vitals.js avec le mode attribution retourne l'élément interactif et la durée du délai pour chaque mesure INP en production, et Lighthouse CI dans GitHub Actions signale les violations du Total Blocking Time (TBT) — une bonne approximation du risque INP en conditions de lab. Commencez par DevTools pour le diagnostic, puis confirmez avec les données RUM.
Oui — c'est beaucoup plus simple à mettre en place dès le début que de l'intégrer sur une codebase existante avec des dettes de performance. Chez Nehos, Lighthouse CI est configuré lors du sprint 0 avec des seuils initialement larges (LCP < 4 s) puis progressivement resserrés à mesure que l'équipe optimise. Sans CI automatisé, les régressions de performance passent invariablement inaperçues entre les sprints.
Oui, c'est une cause fréquente et difficile à détecter. Quand un Server Component rend un contenu qui diffère du rendu client initial (par exemple, un composant qui lit localStorage ou un cookie pendant l'hydratation), React corrige le DOM après hydratation — ce qui provoque un décalage visuel. La solution est de différer le rendu client avec useEffect et d'afficher un skeleton dans l'état initial, ou d'utiliser suppressHydrationWarning sur les éléments dont la valeur peut légitimement différer.
Google confirme officiellement que les CWV sont un facteur de ranking actif depuis 2021. En 2026, l'impact est plus marqué sur les SERP compétitives où plusieurs pages ont une qualité de contenu similaire. L'impact mobile est plus fort que desktop. Mesurez l'effet sur vos propres pages dans la Search Console.
Parlons de votre projet
Un devis chiffré, sans engagement
Décrivez votre besoin en deux minutes : un consultant senior vous répond avec une première estimation et les prochaines étapes.
- Réponse sous 24 h ouvrées
- Sans engagement
- Un consultant senior vous répond