SD
Next.jsPerformanceCore Web VitalsTypeScript

Performances Next.js : boostez vos Core Web Vitals en 5 étapes

Apprenez à optimiser votre application Next.js pour obtenir de meilleurs scores Core Web Vitals : images, fonts, lazy loading et bundle size.

AMAlexis Mouchon8 min de lecture

Les performances web ne sont plus un luxe — c'est une nécessité. Google utilise les Core Web Vitals comme facteur de classement SEO depuis 2021, et vos utilisateurs abandonnent une page qui met plus de 3 secondes à charger. Bonne nouvelle : Next.js est conçu pour être rapide, à condition de l'utiliser correctement. Voici les 5 optimisations qui font vraiment la différence.

Pourquoi les Core Web Vitals sont critiques pour votre projet

Les Core Web Vitals mesurent trois aspects fondamentaux de l'expérience utilisateur :

Un mauvais score sur ces métriques pénalise votre référencement ET dégrade l'expérience de vos visiteurs. Avec Next.js, vous avez tous les outils pour exceller — encore faut-il les utiliser.


1. Optimiser les images avec next/image

L'image est souvent le principal coupable d'un mauvais LCP. Le composant <Image /> de Next.js gère automatiquement :

import Image from 'next/image'

// ✅ Bonne pratique : toujours spécifier width/height ou fill
export function HeroBanner() {
  return (
    <Image
      src="/hero.jpg"
      alt="Bannière principale"
      width={1200}
      height={600}
      priority // Charger en priorité pour le LCP
      quality={85}
    />
  )
}

Astuce critique : utilisez priority sur l'image au-dessus de la ligne de flottaison (le fameux "above the fold"). Sans ça, Next.js la charge en lazy, ce qui dégrade votre LCP.

Pour les images de taille dynamique (grilles, carousels), préférez le mode fill avec un conteneur positionné :

<div className="relative h-64 w-full">
  <Image
    src={photo.url}
    alt={photo.alt}
    fill
    className="object-cover"
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  />
</div>

La prop sizes est essentielle : elle indique au navigateur quelle taille charger selon le viewport, évitant de télécharger une image 1200px pour l'afficher en 300px.


2. Maîtriser le chargement des polices avec next/font

Les polices Google chargées via un simple <link> dans le HTML causent deux problèmes : un FOUT (Flash of Unstyled Text) et une requête réseau externe qui peut bloquer le rendu.

next/font résout tout ça en téléchargeant les polices au moment du build et en les servant depuis votre propre domaine :

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

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Affiche le texte immédiatement avec la font de fallback
  variable: '--font-inter',
})

const firaCode = Fira_Code({
  subsets: ['latin'],
  variable: '--font-fira-code',
  weight: ['400', '500'],
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr" className={`${inter.variable} ${firaCode.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  )
}

Résultat : zéro requête externe, zéro layout shift lié aux polices. Le score CLS s'améliore instantanément.


3. Réduire le bundle avec le dynamic import

Le "bundle splitting" consiste à ne charger que le JavaScript réellement nécessaire à la page en cours. Next.js le fait automatiquement par route, mais vous pouvez aller plus loin avec dynamic() pour les composants lourds.

import dynamic from 'next/dynamic'

// Charger uniquement quand le composant est visible/interagi
const RichTextEditor = dynamic(
  () => import('@/components/RichTextEditor'),
  {
    loading: () => <div className="h-64 animate-pulse bg-muted rounded-lg" />,
    ssr: false, // Pour les composants qui dépendent de window/document
  }
)

const ChartComponent = dynamic(
  () => import('@/components/Chart'),
  { ssr: false }
)

Cas d'usage typiques pour dynamic avec ssr: false :

Pour les modals et drawers, vous pouvez aussi les conditionner à l'état d'ouverture :

'use client'

import { useState } from 'react'
import dynamic from 'next/dynamic'

const Modal = dynamic(() => import('@/components/Modal'))

export function App() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button onClick={() => setOpen(true)}>Ouvrir</button>
      {open && <Modal onClose={() => setOpen(false)} />}
    </>
  )
}

4. Optimiser les requêtes avec le cache de Next.js 15

Depuis Next.js 15, le comportement du cache a été revu. Les fetch ne sont plus mis en cache par défaut — c'est un changement important à intégrer dans votre stratégie.

// app/blog/page.tsx — Server Component

// ✅ Cache statique (généré au build, revalidé toutes les heures)
const posts = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }
})

// ✅ Données toujours fraîches (no-store)
const liveData = await fetch('https://api.example.com/live', {
  cache: 'no-store'
})

// ✅ Cache illimité jusqu'à revalidation manuelle (ISR avec tag)
const config = await fetch('https://api.example.com/config', {
  next: { tags: ['config'] }
})

Pour les données issues de votre backend NestJS via GraphQL, pensez à utiliser unstable_cache pour mettre en cache les résultats de vos resolvers :

import { unstable_cache } from 'next/cache'

const getCachedUser = unstable_cache(
  async (userId: string) => {
    // Votre appel GraphQL ici
    return fetchUserFromGraphQL(userId)
  },
  ['user'],
  { revalidate: 300, tags: ['users'] }
)

5. Analyser et monitorer avec les bons outils

Optimiser sans mesurer, c'est naviguer à l'aveugle. Voici le toolkit que j'utilise systématiquement :

Analyse du bundle

# Installer l'analyseur de bundle
npm install @next/bundle-analyzer --save-dev
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer'

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
})

export default withBundleAnalyzer({
  // votre config Next.js
})
ANALYZE=true npm run build

Cela génère une visualisation interactive de votre bundle. Cherchez les dépendances inattendument lourdes (moment.js, lodash entier, etc.).

Monitoring en production

Next.js intègre nativement le support des Web Vitals :

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/react'

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

En dehors de Vercel, vous pouvez envoyer vos métriques vers votre propre endpoint :

// app/components/WebVitals.tsx
'use client'

import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
  useReportWebVitals((metric) => {
    console.log(metric) // Envoyez vers votre analytics
  })
  return null
}

Récapitulatif : checklist de performance Next.js

Avant de mettre en production, vérifiez ces points :

  1. Images : <Image /> partout, priority sur le hero, sizes configuré
  2. Fonts : next/font à la place des <link> Google Fonts
  3. Bundle : dynamic() pour les composants > 50kb non critiques
  4. Cache : stratégie de revalidation adaptée à chaque route
  5. Monitoring : Web Vitals trackés en production

Avec ces optimisations en place, atteindre un score Lighthouse > 90 sur mobile n'est pas un objectif ambitieux — c'est le résultat normal d'un projet Next.js bien configuré.


Vous travaillez sur un projet Next.js et vous vous battez avec les performances ? N'hésitez pas à me contacter — c'est exactement le genre de problème que j'aime résoudre.