Next.js 16 cacheComponents: Migratie van 91.000 pagina's zonder downtime
Je deploy gaat live om 02:14 uur. Eenenigduizend productpagina's—categoriestructuren, gelokaliseerde varianten in 14 markten, redactionele SEO-content—schakelen van Next.js 14's App Router naar v16's cacheComponents API. Je bekijkt de eerste request waterfall in Vercel's logs. TTFB stijgt naar 1,8 seconden. Je Slack pingt. Achttien maanden lang had je gevochten tegen het oude default-cache model: verouderde prijzen, revalidateTag-aanroepen die te laat werden afgegeven, customer-support tickets die "de website" de schuld gaven. Next.js 15 zette caching standaard uit; v16 gaf je cacheComponents om selectief weer in te schakelen. Je koos voor migratie boven langzame dood door duizend cache-bugs. Nu staar je naar real traffic, real errors, en een production incident twee uur voor zonsopgang. Hier is wat kapot ging, wat standhield, en de performance-verschillen die geen benchmark-suite voorspelde.
Inhoudsopgave
- Het caching-probleem dat we eigenlijk hadden
- Wat veranderde in Next.js 15 en 16
- cacheComponents begrijpen
- Onze migratiestrategie voor 91.000 pagina's
- Implementatie: Stap voor stap
- Performance-resultaten en benchmarks
- Valkuilen en gotchas
- Wanneer je cacheComponents wel en niet moet gebruiken
- FAQ

Het caching-probleem dat we eigenlijk hadden
Laat me het beeld schetsen. In Next.js 14's App Router werden fetch-verzoeken in Server Components standaard gecached. De Data Cache bleef behouden tussen deployments. De Full Route Cache sloeg gerenderde HTML en RSC-payloads op build-tijd op. En de Router Cache aan de clientkant hield prefetched segmenten langer bij dan je zou verwachten.
Voor een site met 91.000 pagina's creëerde deze standaard-alles-cachen benadering twee categorieën problemen:
Verouderde gegevens overal. Productprijzen werden bijgewerkt in onze headless CMS (Sanity, in ons geval) maar de gecachte fetch-resultaten bleven hangen. We hadden revalidateTag-aanroepen verspreid over 47 verschillende server actions. Eentje missen? Een klant ziet gisterens prijs. We hadden letterlijk een Slack-kanaal genaamd #cache-crimes waar het content team verouderde pagina's rapporteerde.
Build-tijden uit de hel. Volledige statische generatie van 91.000 pagina's duurde meer dan 3 uur. We waren naar ISR gegaan met revalidate: 3600 voor de meeste pagina's, maar de interactie tussen ISR, de Data Cache en on-demand revalidatie was werkelijk moeilijk te redeneren. Nieuwe developers in het team zouden hun eerste twee weken alleen maar besteden aan het begrijpen van de caching layers.
De cognitieve kostentaks
Hier is wat ik denk dat mensen onderschatten: de cognitieve kosten van impliciete caching. Wanneer caching de standaard is en je schakelt het uit, vereist elke nieuwe component dat je jezelf afvraagt "moet dit gecached worden?" en dan onthouden dat je de juiste directief toevoegt als het antwoord nee is. Wanneer geen-caching de standaard is en je schakelt het in, denk je alleen over caching wanneer je het actief wilt. Dat is fundamenteel anders—en beter—mentaal model.
Wat veranderde in Next.js 15 en 16
Next.js 15 was de grote filosofische verschuiving. Het team flipte de standaardinstellingen:
| Gedrag | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
fetch() in Server Components |
Gecached standaard | Niet gecached standaard | Niet gecached standaard |
| Route Handlers (GET) | Gecached standaard | Niet gecached standaard | Niet gecached standaard |
| Client Router Cache | 30s (dynamisch) / 5min (statisch) | 0s voor paginasegmenten | 0s standaard, configureerbaar |
| Full Route Cache | Ingeschakeld voor statische routes | Hetzelfde | Hetzelfde, met cacheLife verfijningen |
| Component-level caching | unstable_cache |
use cache directive (experimenteel) |
cacheComponents API (stabiel) |
Next.js 15 introduceerde de use cache directive als experimentele functie achter een vlag. Next.js 16, uitgebracht in begin 2025, stabiliseerde dit als de cacheComponents configuratieoptie en de bijbehorende "use cache" directive, naast cacheLife voor het definiëren van aangepaste cache-profielen en cacheTag voor gerichte invalidatie.
De kernidee: caching verschoof van een impliciete framework-gedrag naar een expliciete developer-keuze op componentniveau. Dit is enorm belangrijk voor grote sites.
cacheComponents begrijpen
De cacheComponents functie in next.config.js schakelt component-level caching in via de "use cache" directive. Hier is de basisinstellingen:
// next.config.js (Next.js 16)
const nextConfig = {
experimental: {
cacheComponents: true,
},
};
module.exports = nextConfig;
Eenmaal ingeschakeld, kun je "use cache" aan de bovenkant van elke async Server Component, server action, of zelfs een layout-bestand toevoegen:
// app/products/[slug]/page.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export default async function ProductPage({ params }: { params: { slug: string } }) {
cacheLife('products'); // custom cache profile
cacheTag(`product-${params.slug}`);
const product = await fetchProduct(params.slug);
return (
<div>
<h1>{product.name}</h1>
<ProductDetails product={product} />
<DynamicPricing productId={product.id} /> {/* This component is NOT cached */}
</div>
);
}
cacheLife-profielen
Hier wordt het interessant voor grote sites. Je definieert benoemde cache-profielen in next.config.js:
const nextConfig = {
experimental: {
cacheComponents: true,
cacheLife: {
products: {
stale: 300, // serve stale for 5 minutes
revalidate: 3600, // revalidate after 1 hour
expire: 86400, // hard expire after 24 hours
},
editorial: {
stale: 3600,
revalidate: 86400,
expire: 604800, // 7 days
},
navigation: {
stale: 86400,
revalidate: 604800,
expire: 2592000, // 30 days
},
},
},
};
Het drielaagse model (stale, revalidate, expire) kaart mooi toe op stale-while-revalidate semantiek. Tijdens het stale venster wordt gecachte content onmiddellijk geserveerd. Na stale maar voor expire, treedt achtergrond revalidatie in werking. Na expire is de cache-entry weg.
cacheTag voor invalidatie
De cacheTag functie vervangt het oude revalidateTag patroon met iets composeerbaars:
import { revalidateTag } from 'next/cache';
// In a webhook handler or server action:
export async function handleProductUpdate(productSlug: string) {
revalidateTag(`product-${productSlug}`);
revalidateTag('product-listing'); // invalidate listing pages too
}
Dit onderdeel veranderde niet veel van Next.js 15, maar het werkt veel beter met cacheComponents omdat je specifieke gecachte componenten tagt in plaats van ondoorzichtige framework-level caches proberen te invalideren.

Onze migratiestrategie voor 91.000 pagina's
We deden dit niet allemaal in één keer. Met 91.000 pagina's in 14 talen zou een big-bang migratie roekeloos zijn geweest. Hier is hoe we het opbrakten:
Fase 1: Upgrade naar Next.js 16, geen cache-wijzigingen (Week 1-2)
We hebben upgegraad van Next.js 14.2 naar 16.0 zonder cacheComponents in te schakelen. Dit alleen al veranderde gedrag omdat fetch-verzoeken niet langer standaard werden gecached. We verwachtten TTFB-regressies en we kregen ze:
- Gemiddelde TTFB steeg van 180ms naar 340ms op productpagina's
- Origin server belasting nam toe met ongeveer 60% (onze Sanity CDN hield stand, maar onze aangepaste API-eindpunten niet)
- ISR revalidatie werd eigenlijk sneller omdat er minder cache state te beheren was
Dit bevestigde wat we vermoedden: we waren zwaar afhankelijk geweest van impliciete caching, en veel van onze pagina's hadden echt caching nodig—alleen expliciet, opzettelijk caching.
Fase 2: Audit en classificatie van pagina's (Week 3)
We categoriseerden elke route in onze app:
| Paginatype | Aantal | Cache-strategie | cacheLife-profiel |
|---|---|---|---|
| Productdetailpagina's | 42.000 | Cache met product tag | products (5min stale / 1hr revalidate) |
| Categorielistingpagina's | 3.200 | Cache met category tag | products (5min stale / 1hr revalidate) |
| Redactionele/blogpagina's | 8.400 | Agressief cachen | editorial (1hr stale / 24hr revalidate) |
| Gelokaliseerde varianten | 31.647 | Hetzelfde als basispagina | Geërfd van base |
| Account/dynamische pagina's | 6.000 | Geen cache | N/A |
Fase 3: cacheComponents inschakelen, directieven toevoegen (Week 4-6)
We schakelden de vlag in en begonnen "use cache" directieven toe te voegen. De sleutelbeslissing: we hebben gecached op paginaniveau voor de meeste routes, maar op componentniveau voor pagina's met gemengde statische/dynamische content.
Voor productpagina's werd de productinfo en afbeeldingen gecached, maar de prijscomponent en voorraadbeschikbaarheid werden niet gecached:
// components/ProductInfo.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export async function ProductInfo({ slug }: { slug: string }) {
cacheLife('products');
cacheTag(`product-${slug}`, 'product-info');
const product = await getProduct(slug);
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductImages images={product.images} />
</section>
);
}
// components/DynamicPricing.tsx
// NO "use cache" directive -- always fresh
export async function DynamicPricing({ productId }: { productId: string }) {
const pricing = await getPricing(productId); // hits pricing API every request
return (
<div className="pricing">
<span className="price">${pricing.current}</span>
{pricing.onSale && <span className="was-price">${pricing.original}</span>}
</div>
);
}
Fase 4: Webhook-integratie (Week 7)
We verdraaid onze Sanity webhooks om revalidateTag aan te roepen met de juiste tags. Dit was eigenlijk eenvoudiger dan onze oude setup omdat tags nu expliciet in de code stonden, niet verspreid over fetch-opties.
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
switch (body._type) {
case 'product':
revalidateTag(`product-${body.slug.current}`);
revalidateTag('product-listing');
break;
case 'category':
revalidateTag(`category-${body.slug.current}`);
revalidateTag('navigation');
break;
case 'article':
revalidateTag(`article-${body.slug.current}`);
break;
}
return new Response('OK', { status: 200 });
}
Implementatie: Stap voor stap
Als je een soortgelijke migratie doet, hier is de praktische playbook die we zouden aanraden (en wat we nu gebruiken voor Next.js development projecten bij Social Animal):
Stap 1: Schakel de vlag in
// next.config.js
module.exports = {
experimental: {
cacheComponents: true,
cacheLife: {
// Start with sensible defaults
default: {
stale: 60,
revalidate: 900,
expire: 86400,
},
},
},
};
Stap 2: Vind je hot paths
Gebruik je analytics om pagina's te identificeren die het meeste verkeer krijgen en waar TTFB het meest uitmaakt. Voor ons waren het categoriepagina's (veel verkeer, relatief stabiele content) en productpagina's (veel verkeer, matig dynamische content).
Stap 3: Voeg `"use cache"` top-down toe
Begin met layouts. Als je root layout navigatiegegevens ophaalt, cache dat eerst—het is de impact met het hoogste effect en het laagste risico:
// app/layout.tsx
// Note: "use cache" on layouts caches the layout shell
// Child pages still render independently
import { Navigation } from '@/components/Navigation';
export default async function RootLayout({ children }) {
return (
<html>
<body>
<Navigation /> {/* This component has its own "use cache" */}
{children}
</body>
</html>
);
}
Stap 4: Stel monitoring in
We gebruikten Vercel's ingebouwde analytics plus custom logging om cache hit rates bij te houden. In de eerste week na het inschakelen van cacheComponents was onze cache hit rate slechts 34%. Na het aanpassen van stale durations steeg het naar 78%.
Performance-resultaten en benchmarks
Hier zijn de echte cijfers na de volledige migratie, gemeten over een 30-daagse periode op Vercel's Pro-plan:
| Metriek | Voor (Next.js 14) | Na fase 1 (v16, geen cache) | Na volledige migratie |
|---|---|---|---|
| Gem. TTFB (productpagina's) | 180ms | 340ms | 95ms |
| Gem. TTFB (categoriepagina's) | 220ms | 410ms | 72ms |
| Gem. TTFB (redactionele pagina's) | 150ms | 280ms | 45ms |
| P99 TTFB (alle pagina's) | 1.200ms | 2.100ms | 380ms |
| Build-tijd (volledig) | 3u 12min | 2u 48min | 48min |
| Vercel function invocations/dag | 2.4M | 3.8M | 1.1M |
| Maandelijkse Vercel factuur | ~$840 | ~$1.200 | ~$520 |
| Cache hit rate | Onbekend (impliciet) | N/A | 78% |
| Verouderde content-incidenten (#cache-crimes) | 8-12/week | 0 | 1-2/month |
De build-tijdverbetering verdient uitleg. Met cacheComponents zijn we af gegaan van het genereren van alle 91.000 pagina's op build-moment. In plaats daarvan hebben we alleen de top 5.000 pagina's (naar verkeer) statisch gegenereerd en de rest on-demand laten genereren met caching. De cacheComponents directive betekende dat die on-demand pagina's werden gecached na het eerste bezoek, waarbij cacheLife verouderde content regelt.
De Vercel-factuur daling was significant. Minder function invocations (vanwege expliciete component caching) plus kortere build-tijden betekenden echte kostenbesparing. Die reductie van ongeveer $320/maand betaalt zichzelf terug.
Valkuilen en gotchas
Serialisatiegrenzen
De "use cache" directive creëert een serialisatiegrens. Alles wat als props in een gecachte component wordt doorgegeven moet serialiseerbaar zijn. We hadden verschillende componenten die callback-functies of React-elementen als props ontvingen—die braken onmiddellijk. De fix was herstructurering om in plaats daarvan compositiepatronen te gebruiken:
// ❌ Dit breekt met "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
// onAddToCart is a function -- not serializable!
}
// ✅ Dit werkt
"use cache";
export async function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
{/* AddToCart is a Client Component, not cached */}
<AddToCartButton productId={product.id} />
</div>
);
}
Dynamische params en cache key explosie
Met 91.000 pagina's, elk met unieke params, is de cache key ruimte enorm. We raakten Vercel's edge cache limieten in de eerste week en moesten strategischer zijn over welke pagina's lange expire waarden kregen. Low-traffic long-tail pagina's kregen kortere cache durations.
De `Date.now()` val
Elke component met "use cache" die Date.now() of new Date() aanroept in de gecachte functie zal die timestamp cachen. We vonden dit in een "last updated" display dat dezelfde tijd voor uren liet zien. De fix: verplaats time-sensitive logica naar een Client Component of een niet-gecachte Server Component.
Geneste cache-grenzen
Wanneer je gecachte componenten in andere gecachte componenten nested, heeft de binnenste cache zijn eigen levenscyclus. Dit is krachtig maar verwarrend. We stelden een team-conventie vast: cache op paginaniveau OF componentniveau, niet beide, tenzij er een duidelijke reden voor is.
Wanneer je cacheComponents wel en niet moet gebruiken
Gebruik het wanneer:
- Je hebt meer dan een paar honderd pagina's en ISR build-tijden zijn pijnlijk
- Je content heeft duidelijke versfheidsvereisten die per sectie variëren
- Je fijnmazig controle nodig hebt over wat wel en niet wordt gecached
- Je op Vercel of een platform draait dat de Next.js cache-lagen ondersteunt
- Je infrastructuurkosten op high-traffic sites wilt reduceren
Gebruik het niet wanneer:
- Je site klein genoeg is dat volledige SSG prima werkt
- Elke pagina is volledig dynamisch (user-specifieke content overal)
- Je niet op een hosting platform zit dat Next.js caching-infrastructuur ondersteunt
- Je team nieuw is in Next.js—kom eerst comfortabel met de basics
Als je evalueert of je project dit niveau van cache-controle nodig heeft, of of een ander framework zoals Astro beter zou passen voor je content-zware site, is dat iets om door te denken voordat je je op een migratie vastlegt.
Voor projecten waar content uit meerdere headless CMS-bronnen komt, werkt het cacheTag systeem in Next.js 16 prachtig met headless CMS-architecturen—elk content type krijgt zijn eigen invalidatie-kanaal.
FAQ
Wat is cacheComponents in Next.js 16?
cacheComponents is een experimentele configuratieoptie in Next.js 16 die de "use cache" directive voor Server Components inschakelt. Het laat je expliciet markeren welke componenten moeten worden gecached en aangepaste cache-profielen definiëren met behulp van cacheLife. Het is de stabiele evolutie van de use cache directive die experimenteel was in Next.js 15.
Hoe verschilt cacheComponents van ISR (Incremental Static Regeneration)?
ISR cached hele pagina's en revalideert ze op basis van een tijdschema. cacheComponents laat je individuele componenten in een pagina cachen, elk met verschillende cache-levensduren. Een enkele pagina kan een header hebben die 24 uur wordt gecached, productinfo die 1 uur wordt gecached, en prijzen die nooit worden gecached. ISR kan dat niet—het is alles of niets op paginaniveau.
Moet ik op Vercel zijn om cacheComponents te gebruiken?
Nee, maar de ervaring is het beste op Vercel omdat de caching-infrastructuur nauw is geïntegreerd. Self-hosted Next.js deployments kunnen cacheComponents gebruiken met de file system cache adapter, maar je krijgt niet de edge distribution voordelen. Platforms als Netlify en Cloudflare voegen ondersteuning toe, maar vanaf midden 2026 blijft Vercel de meest volledige implementatie.
Hoe invalideer ik gecachte componenten in Next.js 16?
Je gebruikt cacheTag() in je gecachte component om tags toe te wijzen, roep dan revalidateTag('tag-name') aan vanuit een server action, route handler, of webhook eindpunt. Dit invalideren alle gecachte componenten met die tag. Het is dezelfde API van Next.js 15, maar het is nuttig omdat je expliciete gecachte componenten tagt in plaats van ondoorzichtige framework caches.
Zal cacheComponents mijn Vercel factuur verlagen?
Het kan significante kosten verminderen. In ons geval daalde function invocations met 54% omdat gecachte component responses van de cache layer werden geserveerd in plaats van serverless functions aan te roepen. De build-tijdvermindering bespaart ook op build minuten. Je resultaten zullen variëren op basis van verkeerspatronen en cache hit rates—controleer Vercel's prijscalculator met je huidige gebruik.
Wat gebeurt er als ik "use cache" aan een component toevoeg die niet-serialiseerbare props ontvangt?
Je krijgt een build fout. De "use cache" directive creëert een serialisatiegrens, dus alle props moeten serialiseerbaar zijn (strings, nummers, gewone objects, arrays). Functies, React-elementen, class-instanties en andere niet-serialiseerbare waarden veroorzaken build-fouten. Herstructureer je component om alleen data props te accepteren en verwerk interactiviteit in onderliggende Client Components.
Kan ik cacheComponents gebruiken met React Server Components van andere frameworks?
Nee. cacheComponents is een Next.js-specifieke functie die bovenop React's Server Components is gebouwd. Terwijl de "use cache" directive syntax uiteindelijk een React standard kan worden, zijn de cacheLife profielen en cacheTag systeem Next.js APIs. Als je een framework als Remix gebruikt of een aangepaste RSC-setup, heb je verschillende cache-strategieën nodig.
Hoe lang duurt het om een grote Next.js site naar cacheComponents te migreren?
Voor onze 91.000-pagina site met een team van 4 developers duurde de volledige migratie 7 weken inclusief testen en monitoring. Een kleinere site (onder de 10.000 pagina's) met een eenvoudiger data model zou dit waarschijnlijk in 1-2 weken kunnen doen. De werkelijke code-wijzigingen zijn eenvoudig—de tijd gaat naar het auditen van je cache-behoeften, het testen van invalidatie flows en het monitoren van cache hit rates na deployment. Als je dit niet alleen wilt doen, neem contact met ons op—we hebben dit al een paar keer gedaan.