Guide Next.js 16
Cache Next.js sur Vercel : des valeurs par défaut sûres pour le contenu public
Mettez en cache une route publique du catalogue d'Acme Shop de manière délibérée, vérifiez chaque couche, et gardez toute réponse personnalisée hors du stockage partagé.
Sur cette page
La mise en cache sur Vercel n'est pas un unique interrupteur. Une application Next.js peut mettre en cache un fetch côté serveur, prérendre la sortie d'une route, et laisser le CDN de Vercel mettre en cache une réponse HTTP complète : ce sont des couches distinctes, aux comportements d'invalidation distincts. Ce guide s'appuie sur le catalogue public d'Acme Shop ; il ne met jamais en cache ses pages de compte, son panier, son tunnel de paiement, ni ses réponses d'API authentifiées.
À l'échelle où fonctionne une couche d'orchestration d'edge managée, une directive de cache mal appliquée n'est jamais un simple bug local : c'est une politique qui continue de s'appliquer silencieusement à chaque visiteur, dans chaque région, jusqu'à ce que quelqu'un le remarque. C'est pourquoi les limites de cache se décident route par route, avant même d'écrire un en-tête ou un tag.
Un cache partagé exige une sortie publique
Un cache partagé peut servir une même réponse stockée à de nombreux visiteurs. N'ajoutez jamais de politique de cache partagé à une sortie qui varie selon la session, l'autorisation, un cookie, un panier, un droit ou l'état d'un autre utilisateur. Gardez ces données dynamiques ou explicitement privées.
Ce que vous allez mettre en place
Ce guide met en cache la liste publique /shop d'Acme Shop pendant cinq minutes dans le cache de données Next.js, et distingue une réponse JSON délibérément publique mise en cache par le CDN de Vercel. Il utilise les options explicites de fetch, ce qui permet de l'utiliser sans activer les Cache Components de Next.js.
Résultat attendu : /shop utilise une politique de cache de données de cinq minutes, tandis qu'un endpoint JSON délibérément public reçoit une politique CDN Vercel courte et séparée ; vous pourrez prouver que la route publique est cacheable et que la route de compte ne l'est pas.
Préalables : un projet Next.js 16 App Router sans Cache Components, une preview Vercel liée, un catalogue amont public, et une révision inoffensive telle que catalogRevision: "2026-07-14T10:00Z". Vérifiez toujours sur la preview avant la production.
| Couche | Ce qu'elle stocke | Politique de départ |
|---|---|---|
| Cache de données Next.js | Le résultat d'un fetch côté serveur ou d'une lecture unstable_cache | Revalidation toutes les 300 secondes |
| Sortie de route Next.js | La page rendue ou le résultat d'un Route Handler | Laisser les besoins de données de la route décider |
| CDN Vercel | Une réponse HTTP publique complète | TTL partagé court et revalidation en arrière-plan |
| Navigateur | La réponse locale d'un visiteur | Séparer du TTL CDN partagé |
Le cache Next.js sur Vercel commence par une frontière claire
Le catalogue est identique pour tous les visiteurs et peut avoir jusqu'à cinq minutes de retard ; un résumé de compte ne l'est pas, même si deux visiteurs demandent la même URL. Notez ces frontières route par route avant d'écrire un en-tête ou un tag : « cette réponse peut-elle être partagée ? » est une question différente de « ces données sont-elles lentes à récupérer ? », et seule la première décide si un cache partagé est sûr. Le CDN Vercel peut mettre en cache la réponse complète de GET /api/public-catalog ; le cache de données Next.js conserve séparément le fetch serveur tagué acme-shop:catalog ; /account suit un fetch no-store qui n'entre jamais dans un cache partagé. Un HIT sur l'une de ces couches ne prouve pas un hit sur l'autre.
Étape 1 : vérifier qu'une route est sûre à mettre en cache
Notez la première URL à mettre en cache et répondez à ces questions avant de modifier le code :
- La sortie est-elle identique pour tous les visiteurs qui peuvent la demander ?
- Évite-t-elle
Set-Cookie, l'autorisation, les lectures de session et les paramètres de requête spécifiques à un utilisateur ? - Un visiteur peut-il tolérer jusqu'à cinq minutes de données de catalogue anciennes ?
- Le responsable applicatif peut-il nommer l'événement qui devra rafraîchir le contenu plus tôt ?
Pour ce guide, utilisez la liste publique /shop d'Acme Shop. Gardez /account, /cart, /checkout et toute route API authentifiée hors du périmètre.
Étape 2 : mettre en cache le fetch du catalogue public de manière explicite
Next.js 16 ne met pas les requêtes fetch en cache par défaut dans ce modèle. Rendez explicites la persistance, la fraîcheur et la cible d'invalidation prévues pour Acme Shop. force-cache autorise un stockage persistant ; revalidate limite sa durée de vie ; le tag reste réservé à une invalidation étroite à la demande.
// lib/acme-shop/catalog.ts
export type Product = { slug: string; name: string; priceCents: number }
export async function getPublicCatalog(): Promise<Product[]> {
const response = await fetch("https://catalog.acme-shop.example/v1/products", {
cache: "force-cache",
next: { revalidate: 300, tags: ["acme-shop:catalog"] },
})
if (!response.ok) throw new Error("Public catalog is unavailable")
return response.json()
}
// app/shop/page.tsx
import { getPublicCatalog } from "@/lib/acme-shop/catalog"
export default async function ShopPage() {
const products = await getPublicCatalog()
return (
<ul>
{products.map((product) => (
<li key={product.slug}>{product.name}</li>
))}
</ul>
)
}
revalidate: 300 est une durée de vie maximale, pas une garantie que chaque région se réchauffe au même instant. La plus petite valeur de revalidation utilisée par une route peut aussi abaisser la fréquence de revalidation de cette route entière.
Ne supposez pas que l'absence de cache: "force-cache" signifie « toujours en direct ». Sans option explicite, le fetch se relance à chaque requête une fois qu'une API de requête (cookies(), headers(), searchParams) rend la route dynamique ; mais sur une route où aucune de ces API n'est atteinte, il ne s'exécute qu'une seule fois, au moment de next build, et son résultat est réutilisé indéfiniment plutôt que rafraîchi selon un planning. Le revalidate: 300 explicite d'Acme Shop évite cette ambiguïté : le fetch se rafraîchit selon un rythme connu, que /shop reste statique ou non.
Étape 3 : définir des valeurs par défaut de cache sûres pour les lectures hors fetch
Toutes les lectures ne passent pas par fetch. La boutique d'Acme Shop interroge aussi directement une base de données pour le badge de stock faible affiché sur /shop. unstable_cache donne à cette requête le même cycle de vie borné dans le temps et piloté par tag que le fetch du catalogue, afin que les deux chemins partagent un seul jeu de valeurs par défaut de cache sûres plutôt que de laisser la requête s'exécuter à chaque appel sans limite.
// lib/acme-shop/inventory.ts
import { unstable_cache } from "next/cache"
import { db } from "@/lib/acme-shop/db"
export const getLowStockBadges = unstable_cache(
async () => db.query.products.findMany({ columns: { slug: true, stockCount: true } }),
["acme-shop:low-stock-badges"],
{ revalidate: 300, tags: ["acme-shop:catalog"] }
)
Réutiliser le tag acme-shop:catalog signifie qu'un seul événement de publication invalide ensemble la liste de produits et le badge de stock ; voir le guide de revalidation pour cet événement. Donnez à unstable_cache un tableau de clé explicite : sa clé dérive sinon des arguments de la fonction, et la même requête appelée avec un filtre différent devient silencieusement une entrée séparée avec sa propre horloge.
Par ailleurs, des appels fetch GET identiques effectués pendant un seul passage de rendu sont automatiquement dédupliqués : appeler le même fetch depuis deux composants ne coûte qu'une seule requête. Cette mémoïsation se réinitialise à la requête suivante, n'a aucun lien avec revalidate, et ne s'applique pas dans les Route Handlers.
Étape 4 : maintenir les données privées hors du chemin partagé
N'essayez pas de faire d'un token de session une partie d'une clé de cache partagée. Acme Shop récupère les données de compte du visiteur courant à chaque requête.
// lib/acme-shop/account.ts
export async function getAccount(accessToken: string) {
const response = await fetch("https://accounts.acme-shop.example/v1/me", {
cache: "no-store",
headers: { Authorization: `Bearer ${accessToken}` },
})
if (!response.ok) throw new Error("Account is unavailable")
return response.json()
}
Lire cookies() ou headers() ne transforme pas une réponse privée en représentation partagée sûre. Alignez la récupération des données, le comportement de la page et les en-têtes HTTP avec l'exigence de confidentialité.
Étape 5 : mettre une réponse HTTP publique en cache au CDN
Le cache de données ne rend pas automatiquement une réponse HTTP entière cacheable par le CDN. Pour la seule API catalogue publique d'Acme Shop, renvoyez une représentation complète avec un TTL spécifique à Vercel et un tag CDN : le navigateur reçoit la politique navigateur courte, tandis que Vercel consomme Vercel-CDN-Cache-Control et retire Vercel-Cache-Tag avant de transmettre la réponse en aval.
// app/api/public-catalog/route.ts
import { getPublicCatalog } from "@/lib/acme-shop/catalog"
export async function GET() {
const products = await getPublicCatalog()
return Response.json(
{ catalogRevision: "2026-07-14T10:00Z", products },
{
headers: {
"Cache-Control": "public, max-age=0, must-revalidate",
"Vercel-CDN-Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
"Vercel-Cache-Tag": "acme-shop:catalog-response",
},
}
)
}
N'ajoutez jamais ces en-têtes si la requête porte un Authorization, si la réponse pose un cookie, si la sortie varie selon le visiteur, ou si le corps contient des données sensibles. Les critères de cacheabilité de Vercel sont plus étroits qu'« ajouter un en-tête » : GET/HEAD uniquement, aucun Range ni Authorization en requête, statut 200, 404, 410, 301, 302, 307 ou 308, corps de moins de 10 Mo (20 en streaming), et aucun Set-Cookie, private, no-cache, no-store ni Vary: *. Un seul échec disqualifie la réponse, quel que soit le TTL fixé.
Pour une politique partagée avec un autre CDN ou WAF, utilisez CDN-Cache-Control plutôt que Vercel-CDN-Cache-Control : celui-ci est consommé au proxy de Vercel et n'en sort jamais.
La mise en cache multi-fournisseurs a besoin d'une seule source de vérité
Chaque fournisseur présent sur ce chemin rapporte son propre taux de hit et son
propre TTL pour la même URL, et aucun d'entre eux, isolément, ne montre ce qu'un
visiteur a réellement reçu. Une couche d'orchestration managée comme MYO réconcilie
l'état x-vercel-cache de Vercel avec celui de chaque autre fournisseur sous une
seule requête, afin qu'une décision prise ici n'entre jamais silencieusement en
conflit avec une politique fixée ailleurs dans la chaîne.
Pour la résilience, stale-if-error peut accompagner s-maxage : stale-if-error=3600 continue de servir le dernier catalogue valide pendant jusqu'à une heure si l'API amont commence à renvoyer des erreurs. Vercel plafonne ces directives à un an ; la mise en cache reste du meilleur effort par région, si bien qu'une route rarement demandée peut être évincée avant la fin de son TTL.
Étape 6 : valider les comportements public, privé et d'échec
Déployez le changement examiné vers une URL de preview. vercel curl gère automatiquement le contournement de la protection de déploiement sur une preview protégée ; c'est actuellement une commande CLI en beta. Pour une preview publique, un simple curl suffit.
vercel curl /api/public-catalog --deployment https://acme-shop-preview.example
vercel curl /api/public-catalog --deployment https://acme-shop-preview.example
vercel httpstat /api/public-catalog --deployment https://acme-shop-preview.example
| Validation | Preuve attendue | Arrêter si |
|---|---|---|
| Route publique positive | Deux réponses GET /api/public-catalog portent la révision attendue ; la seconde peut annoncer HIT ou STALE. | Le corps contient un champ de compte, un cookie ou une valeur spécifique à un visiteur. |
| Route privée négative | Une réponse /account authentifiée est fraîche pour l'utilisateur connecté et sans politique s-maxage partagée. | Deux comptes de test reçoivent le marqueur ou la réponse l'un de l'autre. |
| Comportement d'échec | Une API amont de test hors production renvoie 503 ; la route rapporte l'erreur attendue sans corps fabriqué. | Un échec amont est stocké silencieusement comme une réponse réussie. |
Le CDN de Vercel est régional : un résultat chaud depuis une localisation ne prouve pas que chaque région est chaude. Conservez l'URL, l'heure, les en-têtes, la révision du corps et la localisation de la requête avec l'enregistrement de changement.
Après vérification de la preview, déployez le changement examiné en production :
vercel deploy --prod
Étape 7 : mesurer le bon résultat
Suivez plus qu'un en-tête de statut de cache. Pour la route modifiée, comparez :
- la latence de requête sur l'URL exposée aux visiteurs ;
- le volume de requêtes vers l'origine ou l'API amont ;
- le taux d'erreur et la justesse des réponses ;
- les signalements de contenu obsolète après une mise à jour du catalogue ; et
- le taux de requêtes dynamiques qui ne doivent jamais être partagées.
Testez un trafic représentatif sur plusieurs régions et conservez un petit objet de test public pour les futurs contrôles.
Reprise sans purge large
Si une réponse publique est incorrecte, retirez d'abord l'éligibilité au cache non sûre ou restaurez le déploiement connu comme bon, puis vérifiez le corps et un parcours authentifié. Ce n'est qu'une fois la réponse corrigée que vous invalidez le tag CDN public étroit, via une session liée au projet et examinée :
vercel cache invalidate --tag acme-shop:catalog-response
vercel cache invalidate --tag marque la réponse comme obsolète : la requête suivante la sert encore pendant qu'une copie fraîche se charge en arrière-plan. vercel cache dangerously-delete --tag la supprime purement et simplement, si bien que la requête suivante reçoit un MISS et bloque sur l'origine ; réservez cette commande à une réponse qui ne doit plus jamais être servie.
Pour une valeur incorrecte du cache de données Next.js, utilisez le workflow de revalidation applicative authentifié du guide de revalidation ; une invalidation CDN ne corrige ni une donnée source erronée ni une conception de route non sûre. Évitez vercel cache purge comme première réponse : elle vide le CDN et le cache de données pour l'ensemble du projet et peut augmenter la charge d'origine sur chaque route à la fois.
Diagnostic rapide
| Symptôme | Cause probable | Contrôle étroit ou reprise |
|---|---|---|
x-vercel-cache: MISS sur une page dynamique | Cet en-tête décrit la réponse CDN, pas le fetch côté serveur. | Ajoutez une révision inoffensive au corps avant de changer un TTL. |
Le Cache-Control du navigateur ne porte pas s-maxage | Vercel consomme ses directives propres avant de transmettre la réponse. | Inspectez x-vercel-cache et le corps, pas le seul en-tête navigateur. |
| Le catalogue ne devient jamais cacheable | La réponse porte Set-Cookie, Authorization, private, no-store, Vary: *, ou est trop volumineuse. | Retirez l'éligibilité partagée ou gardez la route dynamique. |
| Une mise à jour produit reste ancienne | Le repli de cinq minutes n'a pas expiré ou l'événement n'a pas revalidé le tag. | Confirmez la source, puis invoquez une fois le webhook authentifié. |
| Le badge de stock faible et la liste de produits divergent | unstable_cache a utilisé une clé ou un tag différent du fetch. | Alignez les deux sur le tag acme-shop:catalog et une clé explicite. |
Avant d'élargir la politique
- Chaque route cacheable a un responsable de données publiques et une cible de fraîcheur documentés.
- Les réponses privées et authentifiées utilisent
no-storeou une conception privée équivalente. - Chaque réponse partagée dispose d'un chemin d'invalidation testé et d'un tag cohérent entre
fetchetunstable_cache. - Les contrôles d'en-tête incluent le corps de la réponse ou un checksum, pas seulement un statut de cache.
- Chaque réponse partagée respecte les critères de cacheabilité de Vercel : méthode, statut, taille et absence de cookie.
- La vérification de preview précède le déploiement de production.
Références faisant autorité
- Mise en cache et revalidation Next.js
- Référence de l'API
fetchNext.js - Référence de l'API
unstable_cacheNext.js - Vercel CDN Cache
- En-têtes Cache-Control Vercel
- Vercel CLI :
vercel cache
Faites du cache Next.js sur Vercel une discipline d'edge managée
Optimi orchestre le cache Next.js sur Vercel avec chaque autre fournisseur de votre chaîne : des valeurs par défaut de cache sûres, une invalidation cohérente, et la visibilité MYO sur la Performance, la Sécurité et la Visibilité de chaque réponse mise en cache.
Discuter de l'architecture de cache