Qu'est-ce que les Core Web Vitals ?

Les Core Web Vitals sont un ensemble de trois metriques definies par Google pour mesurer la qualite de l'experience utilisateur sur une page web. Depuis 2021, ces metriques sont un facteur de classement dans les resultats de recherche Google.

LCP

Largest Contentful Paint

Mesure le temps d'affichage du plus grand element visible dans la zone d'affichage. Objectif : moins de 2,5 secondes.

CLS

Cumulative Layout Shift

Mesure la stabilite visuelle de la page — les decalages inattendus de mise en page. Objectif : score inferieur a 0,1.

INP

Interaction to Next Paint

Mesure le delai entre une interaction utilisateur (clic, touche) et la mise a jour visuelle. Objectif : moins de 200 millisecondes.

Pourquoi ces metriques sont importantes

Google utilise les Core Web Vitals comme signal de classement depuis juin 2021. A contenu equivalent, un site avec de meilleurs Core Web Vitals sera favorise dans les resultats de recherche. Au-dela du SEO, ces metriques mesurent directement l'experience ressentie par l'utilisateur : temps d'attente, stabilite visuelle et reactivite de l'interface.

L'avantage structurel du headless

L'architecture headless offre un avantage structurel pour les Core Web Vitals par rapport a WordPress traditionnel. La separation backend/frontend permet d'optimiser chaque couche independamment.

Mesurer les Core Web Vitals

Avant d'optimiser, il faut mesurer. Plusieurs outils permettent d'evaluer les Core Web Vitals :

Donnees de laboratoire (environnement controle) :

  • Lighthouse (integre dans Chrome DevTools) : audit de performance avec scores detailles
  • PageSpeed Insights : analyse accessible via URL, combine donnees de laboratoire et donnees terrain
  • WebPageTest : tests detailles avec filmstrip et waterfall

Donnees terrain (utilisateurs reels) :

  • Chrome User Experience Report (CrUX) : donnees reelles collectees aupres des utilisateurs Chrome
  • web-vitals (librairie JavaScript) : collecte des metriques dans votre propre outil d'analytics

Pour integrer la collecte des Core Web Vitals dans un projet Next.js :

// app/layout.tsx — Collecte des Web Vitals
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

Optimiser le LCP (Largest Contentful Paint)

Le LCP mesure le temps d'affichage du plus grand element visible : souvent l'image principale ou le titre de la page. L'objectif est d'atteindre un LCP inferieur a 2,5 secondes.

Optimisation des images avec next/image

Le composant next/image applique automatiquement plusieurs optimisations :

import Image from 'next/image';

// Image au-dessus de la ligne de flottaison : charger en priorite
<Image
  src={post._embedded['wp:featuredmedia'][0].source_url}
  alt={post._embedded['wp:featuredmedia'][0].alt_text}
  width={1200}
  height={630}
  priority // Desactive le lazy loading et charge l'image en priorite
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 1200px"
/>

Attribut priority : a utiliser uniquement pour l'image principale visible sans scroll (above the fold). Cette option desactive le lazy loading et ajoute un <link rel="preload"> dans le <head>.

Attribut sizes : indique au navigateur quelle taille d'image charger selon la largeur de l'ecran. Sans cet attribut, le navigateur charge la taille maximale.

Optimisation des polices avec next/font

Le chargement des polices peut retarder le LCP si le texte principal attend la police pour s'afficher. next/font charge les polices de maniere optimale :

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Affiche le texte immediatement avec une police de fallback
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

next/font telecharge la police au moment du build et la sert depuis le meme domaine que le site (self-hosting). Il n'y a aucune requete vers les serveurs Google Fonts, ce qui elimine un aller-retour reseau.

Reduire le temps de reponse serveur

Utiliser ISR au lieu de SSR

Le SSR genere le HTML a chaque requete, ce qui inclut le temps de requete vers l'API WordPress. L'ISR sert une page pre-generee depuis le CDN, avec un TTFB inferieur a 50ms.

Deployer sur un CDN global

Les plateformes comme Vercel, Netlify ou Cloudflare Pages deployent les pages statiques sur un reseau de serveurs mondial. Le visiteur recoit la page depuis le serveur le plus proche geographiquement.

Optimiser les reponses de l'API WordPress

Limitez les champs retournes par l'API pour reduire la taille des reponses et le temps de traitement.

// Recuperer uniquement les champs necessaires
const res = await fetch(
  `${API_URL}/posts?_fields=id,slug,title,excerpt,date&per_page=10`
);

Optimiser le CLS (Cumulative Layout Shift)

Le CLS mesure les decalages inattendus de mise en page. Un score superieur a 0,1 indique que des elements de la page changent de position apres le chargement initial, ce qui degrade l'experience utilisateur.

Causes frequentes de CLS et solutions

Images sans dimensions explicites : le navigateur ne connait pas la taille de l'image avant son chargement, ce qui provoque un decalage lorsqu'elle s'affiche.

// Probleme : pas de dimensions → le navigateur reserve 0px puis recalcule
<img src="/image.jpg" alt="Photo" />

// Solution : next/image impose les dimensions et reserve l'espace
<Image src="/image.jpg" alt="Photo" width={800} height={450} />

Polices web avec flash de texte invisible (FOIT) : la police par defaut a une hauteur de ligne differente de la police web, ce qui provoque un decalage a l'affichage de la police definitive.

/* Solution : font-display swap + metrics override */
/* next/font gere cela automatiquement */

Contenu injecte dynamiquement : les publicites, les embeds et les composants charges en client-side peuvent decaler le contenu existant.

// Solution : reserver l'espace avec une hauteur minimale
<div style={{ minHeight: '250px' }}>
  <DynamicComponent />
</div>

Optimiser l'INP (Interaction to Next Paint)

L'INP mesure le delai entre une interaction utilisateur (clic, touche clavier, tap) et la mise a jour visuelle qui en resulte. L'objectif est un INP inferieur a 200 millisecondes.

Reduire le cout de l'hydratation

En architecture Next.js, les pages SSG et ISR sont d'abord servies en HTML statique, puis React "hydrate" la page pour la rendre interactive. Cette hydratation execute du JavaScript et peut bloquer les interactions.

// Utiliser les Server Components par defaut (pas d'hydratation)
// app/blog/[slug]/page.tsx — Server Component
export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  return (
    <article>
      <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
      <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
      {/* Seul ce composant est hydrate cote client */}
      <LikeButton postId={post.id} />
    </article>
  );
}

Les Server Components de l'App Router n'envoient pas de JavaScript au navigateur. Seuls les composants marques 'use client' sont hydrates. Limitez le nombre de Client Components pour reduire le cout d'hydratation.

Code-splitting et chargement differe

Next.js decoupe automatiquement le JavaScript par route (code-splitting). Pour les composants lourds qui ne sont pas visibles immediatement, utilisez le chargement dynamique :

import dynamic from 'next/dynamic';

// Le composant n'est charge que lorsqu'il est necessaire
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <div style={{ height: '400px' }}>Chargement du graphique...</div>,
  ssr: false, // Ne pas generer cote serveur si le composant est purement interactif
});

Strategies de cache

Le cache est le levier de performance le plus efficace. Plusieurs niveaux de cache s'appliquent dans une architecture headless Next.js :

Configuration des en-tetes de cache

// next.config.js — En-tetes de cache pour les assets statiques
const nextConfig = {
  async headers() {
    return [
      {
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/images/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=86400, stale-while-revalidate=604800',
          },
        ],
      },
    ];
  },
};

Optimiser les reponses de l'API WordPress

La taille et le temps de reponse de l'API WordPress impactent directement le temps de build (SSG) et de regeneration (ISR).

Reduire la taille des reponses :

// Selectionner uniquement les champs necessaires avec _fields
const res = await fetch(`${API_URL}/posts?_fields=id,slug,title,excerpt,featured_media,date`);

// Limiter le nombre de resultats avec per_page
const res = await fetch(`${API_URL}/posts?per_page=10&page=1`);

Cote WordPress : installez un plugin de cache objet (Redis Object Cache ou Memcached) pour accelerer les requetes a la base de donnees. Le plugin WP Super Cache n'est pas utile en headless (il met en cache le HTML PHP, pas les reponses API).

Analyse du bundle JavaScript

Un bundle JavaScript trop volumineux ralentit le chargement et l'hydratation. Analysez la composition du bundle avec @next/bundle-analyzer :

npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer(nextConfig);
# Generer le rapport d'analyse
ANALYZE=true npm run build

Regles pratiques pour la taille du bundle

Pour un site WordPress headless standard, le bundle JavaScript par route ne devrait pas depasser 100 Ko (compresse gzip). Si une route depasse ce seuil, verifiez que les librairies lourdes (editeurs, graphiques, cartes) sont chargees dynamiquement et non incluses dans le bundle principal. Utilisez le tree-shaking en important uniquement les fonctions necessaires (import specifique au lieu d'import global).

Checklist de performance

Checklist Core Web Vitals pour WordPress headless + Next.js

LCP (< 2,5s) :

  • Utiliser next/image avec l'attribut priority pour l'image principale
  • Utiliser next/font pour le chargement optimise des polices
  • Deployer avec ISR sur un CDN global
  • Limiter les champs de l'API WordPress avec _fields

CLS (< 0,1) :

  • Definir les dimensions (width, height) de toutes les images
  • Utiliser next/font avec display: swap pour eviter le FOIT
  • Reserver l'espace des composants charges dynamiquement avec minHeight

INP (< 200ms) :

  • Utiliser les Server Components par defaut (pas d'hydratation)
  • Limiter les Client Components aux elements interactifs
  • Charger dynamiquement les composants lourds avec dynamic()
  • Maintenir le bundle JavaScript par route sous 100 Ko (gzip)

Cache :

  • Configurer les en-tetes Cache-Control pour les assets statiques
  • Utiliser ISR avec revalidation a la demande pour les pages de contenu
  • Installer un cache objet (Redis) sur le serveur WordPress