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 :
- LCP (Largest Contentful Paint) : temps de chargement du plus grand élément visible (cible : < 2,5s)
- INP (Interaction to Next Paint) : réactivité aux interactions (cible : < 200ms)
- CLS (Cumulative Layout Shift) : stabilité visuelle de la page (cible : < 0,1)
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 :
- La conversion en formats modernes (WebP, AVIF)
- Le redimensionnement selon la taille d'affichage
- Le lazy loading natif
- La réservation d'espace (évite le CLS)
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 :
- Éditeurs de texte riche (TipTap, Quill, Slate)
- Bibliothèques de graphiques (Recharts, Chart.js, ApexCharts)
- Cartes interactives (Leaflet, Mapbox)
- Composants qui accèdent à
windowoulocalStorage
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 :
- Images :
<Image />partout,prioritysur le hero,sizesconfiguré - Fonts :
next/fontà la place des<link>Google Fonts - Bundle :
dynamic()pour les composants > 50kb non critiques - Cache : stratégie de revalidation adaptée à chaque route
- 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.