Développement Web

Data fetching et caching dans Next.js 15 : tutoriel pas-a-pas

12 دقائق للقراءة

Avec l’App Router et les Server Components, le data fetching dans Next.js a complètement changé d’esprit. On ne pense plus en termes de useEffect + useState + loading, ni en termes de getServerSideProps ou getStaticProps. On écrit du code asynchrone directement dans les composants serveur, et Next.js orchestre le rendu. C’est plus simple, mais cela impose de bien comprendre ce qui est mis en cache, quand, et comment forcer le rafraîchissement.

Le changement le plus important par rapport à Next.js 14 concerne le caching. En 14, fetch() était cached agressivement par défaut — surprenant pour beaucoup. En 15, l’équipe Next a inversé le défaut : tout est dynamique sauf indication contraire. Ce tutoriel parcourt les patterns de fetching, les quatre niveaux de cache disponibles, et les pièges classiques quand on migre d’un Pages Router ou d’une SPA classique.

Prérequis

  • Un projet Next.js 15.x avec App Router activé
  • Comprendre Server vs Client Components (sinon, lire le tutoriel dédié avant)
  • Niveau : intermédiaire à avancé
  • Temps estimé : 90 minutes en suivant les exemples

Pour pouvoir reproduire les exemples, il faut une API ou une base accessible. On utilise ici l’API publique https://jsonplaceholder.typicode.com pour les fetch externes, et on simule une base locale via une fonction qui retourne un délai.

Étape 1 — Le fetch simple dans un Server Component

Le cas le plus basique : on charge une liste depuis une API REST et on l’affiche. Tout se passe dans le composant serveur — pas de useState, pas de useEffect, juste un await.

// src/app/articles/page.tsx
type Article = { id: number; title: string; body: string };

export default async function ListeArticles() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const articles: Article[] = await res.json();

  return (
    <section>
      <h1 className="text-2xl font-bold mb-4">Articles</h1>
      <ul className="space-y-3">
        {articles.slice(0, 10).map((a) => (
          <li key={a.id} className="border-b pb-2">
            <h2 className="font-semibold">{a.title}</h2>
            <p className="text-sm text-gray-600 line-clamp-2">{a.body}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

Visitez /articles. La page s’affiche avec les 10 premiers articles. Le serveur a fait l’appel HTTP, attendu la réponse, généré le HTML, puis envoyé le tout au client. Aucun JavaScript spécifique à la page n’a été chargé côté navigateur. Si vous rechargez plusieurs fois, le fetch est ré-exécuté à chaque requête — c’est le comportement par défaut en Next.js 15. La page est donc dynamique.

Étape 2 — Cacher explicitement avec revalidate

Souvent, on n’a pas besoin de données ultra-fraîches. Une liste d’articles publiés par jour peut largement être cachée pendant quelques minutes. On l’indique en passant l’option next.revalidate au fetch.

const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
  next: { revalidate: 60 }, // re-fetch au plus toutes les 60 secondes
});

Avec ce flag, Next.js met en cache la réponse pendant 60 secondes. Le premier visiteur déclenche le fetch ; tous les visiteurs suivants servent depuis le cache. Au bout de 60 secondes, la prochaine requête sert encore le cache stale mais déclenche un re-fetch en arrière-plan (stratégie stale-while-revalidate). Le visiteur après celui-là verra la nouvelle donnée.

Pour mettre en cache indéfiniment jusqu’à invalidation manuelle, utiliser revalidate: false avec un tag qu’on pourra invalider plus tard.

const res = await fetch('https://api.exemple.com/produits', {
  next: { revalidate: false, tags: ['produits'] },
});

Ailleurs dans l’app, on déclenche l’invalidation programmée — typiquement depuis une Server Action quand on crée ou modifie un produit.

// quelque part dans une Server Action
import { revalidateTag } from 'next/cache';

revalidateTag('produits'); // toutes les routes qui fetchent avec ce tag re-fetchent

C’est plus précis que revalidatePath, qui invalide toutes les fetches d’une route donnée sans distinguer leur source. Préférer les tags dès que plusieurs sources de données coexistent sur la même page.

Étape 3 — Le rendu statique vs dynamique

Une page est considérée comme statique par Next.js si toutes ses sources de données peuvent être résolues au moment du build. Elle est dynamique si au moins une source dépend de la requête (cookies, headers, searchParams, fetch sans cache).

Pour forcer le mode statique, utiliser revalidate sur tous les fetchs et éviter d’appeler des fonctions dynamiques comme cookies() ou headers(). Pour forcer le mode dynamique explicitement, exporter export const dynamic = 'force-dynamic' en haut du fichier page.

// src/app/dashboard/page.tsx
export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  // toujours rendue à la requête, même si tout est cachable
}

L’inverse existe : export const dynamic = 'force-static' force le build statique et casse explicitement si le code utilise une API dynamique. Utile pour s’assurer qu’une page de marketing ne devienne pas accidentellement dynamique au fil des changements de code.

Pour vérifier ce que Next.js considère comme statique ou dynamique, lancer pnpm build et regarder la légende dans la sortie : = statique pré-rendue, = statique générée à la demande, ƒ = dynamique server-side. Les développeurs qui viennent de Next.js 14 sont souvent surpris du nombre de routes désormais marquées ƒ.

Étape 4 — Fetches parallèles avec Promise.all

Par défaut, si une page contient plusieurs await consécutifs, ils s’exécutent en série. Cela ralentit inutilement le rendu.

// Lent — execute en serie (200ms + 300ms = 500ms)
const utilisateur = await getUtilisateur(id);
const commandes = await getCommandes(id);

La bonne pratique : déclencher les promesses sans await, puis attendre toutes ensemble.

// Rapide — execute en parallele (max(200ms, 300ms) = 300ms)
const utilisateurPromise = getUtilisateur(id);
const commandesPromise = getCommandes(id);
const [utilisateur, commandes] = await Promise.all([
  utilisateurPromise,
  commandesPromise,
]);

Pour aller plus loin, on peut preload une donnée dans un composant parent pendant que le composant enfant n’est pas encore monté. Cela permet de démarrer la requête tôt même si l’affichage est conditionné.

Étape 5 — Le pattern Suspense + Streaming

Au lieu d’attendre que toute la page soit prête, on peut envoyer du HTML au navigateur dès que les sections rapides sont rendues, et streamer les sections lentes au fur et à mesure. Cela demande deux ingrédients : envelopper les composants async dans <Suspense>, et fournir un fallback.

// src/app/produits/[slug]/page.tsx
import { Suspense } from 'react';
import { FicheProduit } from '@/components/FicheProduit';
import { ListeAvis } from '@/components/ListeAvis';

export default function PageProduit({ params }: { params: Promise<{ slug: string }> }) {
  return (
    <div>
      <FicheProduit params={params} />
      <Suspense fallback={<p>Chargement des avis...</p>}>
        <ListeAvis params={params} />
      </Suspense>
    </div>
  );
}

Si ListeAvis met 2 secondes à charger (requête lente côté base), l’utilisateur voit la fiche produit immédiatement, puis les avis apparaissent. Bien meilleur ressenti qu’un loader qui bloque toute la page. Le navigateur reçoit le HTML en plusieurs morceaux successifs grâce au streaming HTTP.

Astuce : nommer la requête lente avec fetch(...) + signal pour pouvoir annuler si l’utilisateur quitte la page. Next.js 15 propage automatiquement AbortSignal dans les Server Components grâce au nouveau hook cacheSignal de React 19.2.

Étape 6 — Déduplication avec React cache()

Si plusieurs composants de la même page font le même fetch ou la même requête base, Next.js les dédupliquerait automatiquement pour les fetch mais pas pour les appels arbitraires. Pour dédupliquer une fonction quelconque pendant la durée d’une requête, utiliser cache de React.

// src/lib/db.ts
import { cache } from 'react';
import { prisma } from './prisma';

export const getUtilisateur = cache(async (id: string) => {
  return prisma.user.findUnique({ where: { id } });
});

Maintenant, peu importe combien de composants appellent getUtilisateur('abc') pendant le rendu d’une même requête, la base n’est interrogée qu’une seule fois. Le résultat est mémorisé en mémoire pour la durée de la requête, puis oublié. C’est exactement ce qu’on veut pour des données fréquemment consommées (utilisateur courant, panier, configuration).

Étape 7 — La directive ‘use cache’ (expérimental)

Next.js 15 introduit, en mode expérimental, une nouvelle directive : 'use cache'. Elle permet de marquer une fonction ou un composant entier comme cacheable, sans passer par les options de fetch. Activable dans next.config.js via experimental.useCache: true.

// src/lib/produits.ts
'use cache';

export async function getProduitsPopulaires() {
  // Cette fonction est mise en cache automatiquement
  const res = await prisma.produit.findMany({
    orderBy: { ventes: 'desc' },
    take: 10,
  });
  return res;
}

Pour contrôler la durée de vie du cache, ajouter cacheLife('hours') en début de fonction, avec des profils prédéfinis (seconds, minutes, hours, days, weeks, max) ou un objet personnalisé. Pour invalider, utiliser cacheTag('produits-populaires') dans la fonction et appeler revalidateTag depuis ailleurs.

Cette directive est encore expérimentale et son API peut évoluer. Pour la production, rester sur fetch + next: { revalidate, tags } tant que 'use cache' n’est pas marqué stable. C’est en revanche le sens de l’histoire pour les versions ultérieures.

Étape 8 — Invalidation depuis une Server Action

Le cycle de vie complet — fetch caché, modification, invalidation — se joue typiquement dans le contexte d’une Server Action. Voici un exemple condensé.

// src/app/admin/produits/page.tsx
import { revalidateTag } from 'next/cache';

async function ajouterProduit(formData: FormData) {
  'use server';
  const nom = formData.get('nom') as string;
  await prisma.produit.create({ data: { nom } });
  revalidateTag('produits'); // invalide tous les fetches taggés "produits"
}

export default async function Admin() {
  const produits = await fetch('https://api.exemple.com/produits', {
    next: { tags: ['produits'] },
  }).then((r) => r.json());

  return (
    <>
      <form action={ajouterProduit}>
        <input name="nom" />
        <button>Ajouter</button>
      </form>
      <ul>{produits.map((p: any) => <li key={p.id}>{p.nom}</li>)}</ul>
    </>
  );
}

Après soumission du formulaire, la Server Action crée le produit puis appelle revalidateTag('produits'). Toutes les pages qui ont fetché avec ce tag re-fetcheront au prochain accès. La page courante elle-même est aussi automatiquement rafraîchie. C’est le cycle complet sans une seule ligne de gestion d’état client.

Erreurs fréquentes

Erreur Cause Solution
Données qui restent figées indéfiniment Code écrit pour Next.js 14 où fetch cachait par défaut, ou usage de force-static Vérifier qu’on est bien sur 15.x ; auditer les fetch et retirer next: { revalidate: false } si non voulu
Page marquée dynamique alors qu’on voulait du statique Appel à cookies(), headers(), ou un fetch sans option de cache Identifier la source via pnpm build + log ; cacher explicitement ou retirer l’API dynamique
Fetches qui s’enchaînent en série sans raison await en cascade au lieu de Promise.all Refactoriser pour démarrer toutes les promesses en parallèle
Cache qui ne s’invalide jamais malgré revalidateTag Le tag n’est pas attaché au fetch d’origine Vérifier qu’on a bien next: { tags: ['x'] } sur le fetch ; le tag est sensible à la casse
« Route is configured with both dynamic and revalidate » Conflit entre dynamic = 'force-static' et un fetch dynamique Choisir l’un ou l’autre selon le besoin
Suspense fallback qui ne s’affiche jamais Le composant enveloppé n’est pas vraiment async, ou la donnée est en cache Confirmer avec un setTimeout dans la fonction de fetch pour tester

Étape 9 — Choisir la bonne durée de cache selon le type de donnée

L’erreur fréquente consiste à utiliser revalidate: 60 partout par habitude, sans réfléchir au profil réel de la donnée. Voici les cinq cas qui couvrent 95 % des situations rencontrées dans une application web.

Type de donnée Durée recommandée Stratégie
Catalogue de produits (changements quotidiens) 3600 (1 heure) avec tag Tag produits invalidé sur création/édition
Page d’accueil éditoriale 86400 (24 h) avec tag Invalidation manuelle via Server Action admin
Profil utilisateur connecté Pas de cache (dynamic) Chaque requête recharge depuis la base
Données analytics (statistiques agrégées) 300 (5 min) Acceptable d’être un peu en retard, gros gain CPU
Liste de catégories (rarement modifiée) revalidate: false + tag Cache infini, invalidation manuelle

Règle utile : si la donnée change moins d’une fois par heure et que les utilisateurs peuvent tolérer cinq minutes de retard, cacher avec revalidate. Si elle change à chaque interaction utilisateur (panier, notifications, messages), ne pas cacher du tout. Le doute profite au non-cache — un bug de fraîcheur de donnée est plus visible et plus dommageable qu’une légère lenteur côté serveur.

Étape 10 — Débugger le cache en pratique

Pour comprendre ce qui se passe vraiment, deux techniques simples. La première : ajouter console.log dans les fonctions de fetch. En développement, on voit clairement si l’appel se déclenche ou non. La deuxième : examiner les headers de réponse en production.

curl -I https://votre-app.com/produits | grep -i cache
# x-vercel-cache: HIT  → la page a été servie depuis le cache CDN
# x-nextjs-cache: STALE → cache stale-while-revalidate en cours de refresh
# x-vercel-cache: MISS → première requête ou cache expiré

Sur Vercel, l’onglet Observability du dashboard montre directement les ratios de cache par route. Un ratio inférieur à 80 % sur une page supposée statique est un signal d’alerte — il y a presque toujours une API dynamique cachée qui force le rendu à la requête.

Pour aller plus loin

Cet article fait partie d’une série sur React 19 et Next.js 15. Pour la vue d’ensemble et le parcours conseillé, consultez React 19 + Next.js 15 : architecture full-stack moderne.

Tutoriels frères :

Ressources officielles :

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité