Journal / Développement de produits web

Comment fonctionne le cache dans Next.js v13+ avec l'App Router

Comment fonctionne le cache dans Next.js v13+ avec l'App Router

Un tour d'horizon détaillé du fonctionnement du cache dans Next.js 13+, des types de cache qui existent et de leur effet sur la performance et le SEO.

Qu'est-ce que le cache et comment fonctionne-t-il dans Next.js

Le cache sert à conserver les résultats de calculs ou de requêtes afin de ne pas avoir à les refaire, ce qui accélère l'application. Dans Next.js, les pages HTML, les données, les requêtes, le rendu des composants et même des routes entières peuvent être mis en cache.

Avec l'arrivée de l'App Router (à partir de Next.js 13), la philosophie a changé : le cache est désormais activé par défaut, et c'est au développeur de le désactiver lorsqu'un comportement dynamique est nécessaire.

La politique de Next.js est la suivante :

« On met tout en cache, sauf indication contraire »

Si vous ne précisez pas explicitement cache: 'no-store', next: { revalidate } ou dynamic = 'force-dynamic', alors :

  • fetch() sera mis en cache
  • le layout et la page peuvent aussi être mis en cache
  • les routes seront ajoutées au cache des routes
  • le rendu de la page passera dans le Full Route Cache

Cela garantit la performance, mais peut conduire à des données périmées et à des problèmes de débogage.

Types de cache dans Next.js

Source : documentation de Next.js

Full Route Cache (cache de routes côté serveur)

Utilisé avec l'ISR (Incremental Static Regeneration).

  • Toute la page est mise en cache (HTML, données de layout.tsx, page.tsx, fetch(), headers(), cookies()).
  • Le cache se trouve sur le serveur.
  • Fonctionne par défaut quand il n'y a pas de comportement dynamique.
  • Désactivé avec revalidate ou revalidateTag().

Idéal pour les blogs, les pages marketing et les autres contenus qui changent rarement.

Exemple :

export const revalidate = 60; // regenerate the page every 60 seconds

Data Cache (cache de récupération de données côté serveur)

  • Les réponses de fetch() dans les Server Components sont mises en cache.
  • Contrôlé avec next: { revalidate } ou cache: 'no-store'.

Exemple :

await fetch('https://api.com/posts', {
  next: { revalidate: 300 },
});

Request Memoization (cache par requête, côté serveur)

C'est une optimisation interne de Next.js : un même fetch() ou une même fonction avec les mêmes arguments n'est appelé qu'une seule fois par rendu.

  • Pendant un rendu serveur, Next.js suit les requêtes identiques.
  • Si elles ont déjà été appelées, le résultat est récupéré depuis le cache.
  • Fonctionne indépendamment de cache: 'force-cache' ou no-store.

Utile pour :

  • DRY (ne vous répétez pas) quand vous récupérez les mêmes données dans différents composants.
  • Améliorer la performance sans optimisation manuelle.
  • Quand le même fetch est utilisé à la fois dans layout.tsx et page.tsx.

Pas pour :

  • La réutilisation entre différentes requêtes ou utilisateurs (ce n'est pas un cache global).
  • Contourner le cache de récupération de données : il fonctionne sur la base de la Request Memoization.
// lib/getUser.ts
export async function getUser() {
  return fetch('https://api.com/user').then(res => res.json());
}

// app/layout.tsx
export default async function Layout({ children }) {
  const user = await getUser(); // Call 1
  return (
    <div>
      <Sidebar user={user} />
      {children}
    </div>
  );
}

// app/page.tsx
import { getUser } from '../lib/getUser';

export default async function Page() {
  const user = await getUser(); // Call 2, but actually not repeated
  return <p>Hello, {user.name}</p>;
}

Physiquement, le fetch ne sera appelé qu'une seule fois, et le résultat sera utilisé aux deux endroits.

Router Cache (cache côté client)

C'est le cache que vous contrôlez vous-même avec des bibliothèques comme SWR, React Query et d'autres.

  • Fonctionne uniquement avec la navigation côté client (<Link>, router.push()).
  • Next.js ne contrôle pas ce cache : c'est vous qui décidez comment le conserver.
  • Très rapide, car rien n'est re-rendu : React se contente de monter depuis la mémoire.
  • Mais il n'existe que jusqu'à ce que l'utilisateur recharge la page (F5).

Idéal pour les composants interactifs, les formulaires, les tableaux de bord.

Exemple :

'use client';
import useSWR from 'swr';

const { data } = useSWR('/api/data', fetcher);

Comparaison visuelle des types de cache

Type de cacheOù il agitContrôleExemple
Full Route CacheServeur (toute la route)revalidate, tagexport const revalidate = 60
Data CacheServeur (fetch)cache, revalidatefetch(..., { next: ... })
Cache de génération statiqueServeur (au build)AucunJSX sans données
Cache côté clientNavigateur / clientVousSWR / React Query

Un exemple de la vie courante

Supposons que vous construisiez une boutique :

PageQuoi mettre en cacheQuel type de cache utiliser
/produktyListe de produits, mise à jour toutes les 5 minutesFull Route Cache (ISR)
/api/produktyDonnées de l'API produitData Cache (avec revalidateTag)
Navigation SPANavigation rapideRouter Cache (cache côté client)

Problèmes de cache courants dans l'App Router

Données mises à jour, mais la page affiche les anciennes

fetch() utilise force-cache par défaut si vous ne précisez pas cache: 'no-store' ou next: { revalidate }. Résultat : Next.js mémorise les données et ne les met jamais à jour.

Solution :

  • Ajoutez cache: 'no-store' pour désactiver la mise en cache
  • Ou next: { revalidate: 60 } pour l'ISR
fetch('https://api.com/data', { next: { revalidate: 60 } });

L'URL (searchParams) a changé, mais les données sont les mêmes

Next.js ne relance pas la requête de récupération si l'URL est la même. Si vous utilisez des paramètres, ajoutez-les directement à l'URL du fetch :

fetch(`https://api.com/posts?category=${searchParams.category}`, {
  next: { revalidate: 60 }
});

Navigation avec <Link>, mais les données ne se sont pas mises à jour

C'est le Router Cache (cache côté client) qui a été utilisé. Next.js n'envoie pas de nouvelle requête quand on revient sur la page.

Solution :

  • Forcez le rafraîchissement des données avec router.refresh()
  • Utilisez cache: 'no-store' si les données doivent toujours être fraîches

revalidatePath() ou revalidateTag() ne fonctionne pas

  • Le cache n'a pas été créé (par exemple à cause de no-store)
  • Vous n'utilisez pas de tags dans le fetch
  • Vous appelez revalidatePath() en dehors d'une Server Action

Solution :

  • Assurez-vous que le fetch utilise next: { tags: [...] }
  • Appelez revalidateTag('tag') dans une fonction serveur

Le layout est mis en cache et ne se met pas à jour

layout.tsx est lui aussi mis en cache en tant que RSC avec revalidate, et si les données qu'il contient sont en cache, elles ne seront pas mises à jour avant la prochaine ISR.

Solution :

  • Utilisez cache: 'no-store' pour le fetch dans layout.tsx s'il contient, par exemple, le profil de l'utilisateur
  • Ou déplacez le fetch du layout vers la page (ou vers un composant client avec SWR)

Comportement inattendu en mode développement (dev)

Dans next dev, de nombreux caches sont désactivés ou fonctionnent différemment, et revalidate ne fonctionne pas de façon fiable en mode développement.

Conseil :

Testez le cache avec next build && next start ou sur une préversion Vercel.

La combinaison de cookies(), headers(), params et fetch() casse le cache.

Quand vous utilisez des éléments dynamiques comme :

cookies()

headers()

useSearchParams()

generateMetadata() avec des valeurs dynamiques

Next.js désactive automatiquement le cache de la page et passe en dynamic = 'force-dynamic'. Liste de contrôle : comment éviter les problèmes de cache

Définissez toujours clairement le comportement du fetch :

  • N'oubliez pas d'inclure directement les paramètres de requête dans l'URL du fetch
  // no cache at all
  fetch(..., { cache: 'no-store' })

  // ISR (revalidate every 60 seconds)
  fetch(..., { next: { revalidate: 60 } })

  // manual invalidation via tag
  fetch(..., { next: { tags: ['products'] } })
  • N'oubliez pas de placer les paramètres de requête directement dans l'URL transmise à fetch
  • Testez avec next build && next start, sinon vous risquez d'avoir un faux sentiment de sécurité
  • Si vous devez invalider les données manuellement, utilisez revalidatePath() ou revalidateTag()

Si le cache de votre projet sur l'App Router tourne déjà en production et pose problème, ce sont précisément les missions que je prends en tant que développeur Next.js freelance : audits de cache, correctifs et migrations vers l'App Router.