Next.js 16 cacheComponents: 91.000 pagina's migreren van App Router Caching
Next.js 16 cacheComponents: 91.000 pagina's migreren van App Router Caching
We hadden een grote e-commerce-catalogus ongeveer anderhalf jaar op Next.js 14's App Router draaien toen Next.js 16 uitkwam. 91.247 pagina's. Productlijsten, categoriestructuren, redactionele inhoud, gelokaliseerde varianten in 14 markten. Het oude cachingmodel -- waarbij Server Components standaard werden gecacht -- was een mijnenveld van verouderde data en revalidateTag spaghetti geworden. Toen het Next.js-team cacheComponents aankondigde en de verschuiving naar geen-caching-standaard in Next.js 15 (doorgevoerd en verfijnd in v16), wisten we dat het tijd was. Dit is het verhaal van die migratie: wat werkte, wat niet, en de prestatiecijfers daarna.
Inhoudsopgave
- Het cachingprobleem dat we werkelijk hadden
- Wat er veranderde in Next.js 15 en 16
- cacheComponents begrijpen
- Onze migratiestrategie voor 91.000 pagina's
- Implementatie: stap voor stap
- Prestatieresultaten en benchmarks
- Valkuilen en voetangels
- Wanneer u cacheComponents wel en niet moet gebruiken
- Veelgestelde vragen

Het cachingprobleem dat we werkelijk hadden
Laat me het schilderen. In Next.js 14's App Router werden fetch-verzoeken in Server Components standaard gecacht. De Data Cache bleef behouden over implementaties heen. De Full Route Cache sleurde rendered HTML en RSC payloads op build-time op. En de Router Cache aan de clientkant hield prefetched segmenten rond voor... nou ja, langer dan je zou verwachten.
Voor een site met 91.000 pagina's creëerde deze standaard-cache-alles-aanpak twee categorieën problemen:
Verouderde data 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. Mis je één tag? Een klant ziet de prijs van gisteren. We hadden letterlijk een Slack-kanaal genaamd #cache-crimes waar het content-team verouderde pagina's rapporteerde.
Bouwtijden uit de hel. Het volledig statisch genereren van 91.000 pagina's duurde meer dan 3 uur. We waren verhuisd naar ISR met revalidate: 3600 voor de meeste pagina's, maar de interactie tussen ISR, de Data Cache en on-demand revalidatie was werkelijk moeilijk om na te denken. Nieuwe developers op het team zouden hun eerste twee weken doorbrengen met alleen maar het begrijpen van de cachinglagen.
De mentale belastingtol
Hier denk ik dat mensen onderschatten: de cognitieve kosten van impliciete caching. Wanneer caching de standaard is en je opts uit, vereist elke nieuwe component dat je jezelf afvraagt "moet dit gecacht worden?" en dan moet je onthouden om de juiste instructie toe te voegen als het antwoord nee is. Wanneer geen-caching de standaard is en je opts in, denk je alleen over caching wanneer je het actief wilt. Dat is een fundamenteel ander -- en beter -- mentaal model.
Wat er veranderde in Next.js 15 en 16
Next.js 15 was de grote filosofische verschuiving. Het team draaide de standaardinstellingen om:
| Gedrag | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
fetch() in Server Components |
Gecacht standaard | Niet gecacht standaard | Niet gecacht standaard |
| Route Handlers (GET) | Gecacht standaard | Niet gecacht standaard | Niet gecacht standaard |
| Client Router Cache | 30s (dynamisch) / 5min (statisch) | 0s voor page segments | 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 een experimenteel onderdeel achter een vlag. Next.js 16, uitgebracht begin 2025, stabiliseerde dit als de cacheComponents configuratieoptie en de bijbehorende "use cache" directive, samen met cacheLife voor het definiëren van aangepaste cacheprofielen en cacheTag voor gerichte ongeldigmaking.
Het sleutelinzicht: caching verschoof van een impliciete framework-gedrag naar een expliciete keuze van de ontwikkelaar op componentniveau. Dit is een groot verschil 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 basis-setup:
// next.config.js (Next.js 16)
const nextConfig = {
experimental: {
cacheComponents: true,
},
};
module.exports = nextConfig;
Eenmaal ingeschakeld, kunt u "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'); // aangepast cacheprofiel
cacheTag(`product-${params.slug}`);
const product = await fetchProduct(params.slug);
return (
<div>
<h1>{product.name}</h1>
<ProductDetails product={product} />
<DynamicPricing productId={product.id} /> {/* Deze component is NIET gecacht */}
</div>
);
}
cacheLife Profielen
Dit is waar het interessant wordt voor grote sites. U definieert benoemde cacheprofielen in next.config.js:
const nextConfig = {
experimental: {
cacheComponents: true,
cacheLife: {
products: {
stale: 300, // serveer stale voor 5 minuten
revalidate: 3600, // revalideer na 1 uur
expire: 86400, // hard verlopen na 24 uur
},
editorial: {
stale: 3600,
revalidate: 86400,
expire: 604800, // 7 dagen
},
navigation: {
stale: 86400,
revalidate: 604800,
expire: 2592000, // 30 dagen
},
},
},
};
Het drie-laags model (stale, revalidate, expire) kaart mooi op stale-while-revalidate semantiek. Tijdens het stale venster wordt gecachte inhoud onmiddellijk geserveerd. Na stale maar voor expire begint een achtergrondrevalidatie. Na expire is de cache-invoer weg.
cacheTag voor ongeldigmaking
De cacheTag functie vervangt het oude revalidateTag patroon met iets composeerbaars:
import { revalidateTag } from 'next/cache';
// In een webhook-handler of server action:
export async function handleProductUpdate(productSlug: string) {
revalidateTag(`product-${productSlug}`);
revalidateTag('product-listing'); // ongeldig maken listing-pagina's ook
}
Dit deel veranderde niet veel van Next.js 15, maar het werkt veel beter met cacheComponents omdat u specifieke gecachte componenten taggt in plaats van ondoorzichtige framework-level caches ongeldig te maken.

Onze migratiestrategie voor 91.000 pagina's
We hebben dit niet in één keer gedaan. Met 91.000 pagina's in 14 taalgebieden zou een big-bang migratie roekeloos zijn geweest. Hier is hoe we het hebben opgedeeld:
Fase 1: Upgrade naar Next.js 16, geen cachewijzigingen (Week 1-2)
We upgraden van Next.js 14.2 naar 16.0 zonder cacheComponents in te schakelen. Dit alleen al veranderde de gedrag omdat fetch-verzoeken niet langer standaard werden gecacht. We verwachtten TTFB-regressies en we kregen ze:
- Gemiddelde TTFB ging van 180ms naar 340ms op productpagina's
- Origin server load steeg met ongeveer 60% (onze Sanity CDN hield het goed vol, maar onze aangepaste API-eindpunten niet)
- ISR revalidatie werd eigenlijk sneller omdat er minder cache-status te beheren was
Dit bevestigde wat we vermoedden: we waren zwaar op impliciete caching gaan vertrouwen, en veel van onze pagina's hadden werkelijk caching nodig -- gewoon expliciete, intentionele caching.
Fase 2: Audit en classificeer pagina's (Week 3)
We categoriseerden elke route in onze app:
| Paginatype | Aantal | Cache-strategie | cacheLife Profiel |
|---|---|---|---|
| Product detail pagina's | 42.000 | Cache met product tag | products (5min stale / 1uur revalideer) |
| Categorie listing pagina's | 3.200 | Cache met category tag | products (5min stale / 1uur revalideer) |
| Redactionele/blog pagina's | 8.400 | Cache agressief | editorial (1uur stale / 24uur revalideer) |
| Gelokaliseerde varianten | 31.647 | Hetzelfde als basiswagenpagina | Overgeërfd van basis |
| Account/dynamische pagina's | 6.000 | Geen cache | N/A |
Fase 3: cacheComponents inschakelen, richtlijnen toevoegen (Week 4-6)
We schakelden de vlag in en begonnen "use cache" directives toe te voegen. De sleutelbeslissing: we cachten op paginaniveau voor de meeste routes, maar op componentniveau voor pagina's met gemengde statische/dynamische inhoud.
Voor productpagina's waren de productinfo en afbeeldingen gecacht, maar de prijscomponent en voorraaadstatus waren niet gecacht:
// 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
// GEEN "use cache" directive -- altijd vers
export async function DynamicPricing({ productId }: { productId: string }) {
const pricing = await getPricing(productId); // raakt prijzen-API elke 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 hebben onze Sanity-webhooks opnieuw bedraden om revalidateTag te bellen met de juiste tags. Dit was eigenlijk eenvoudiger dan onze oude setup omdat tags nu expliciet in de code waren, 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 u een vergelijkbare migratie doet, hier is het praktische draaiboek dat we zouden aanraden (en wat we nu gebruiken voor Next.js-ontwikkelingen):
Stap 1: De vlag inschakelen
// next.config.js
module.exports = {
experimental: {
cacheComponents: true,
cacheLife: {
// Begin met zinvolle standaardinstellingen
default: {
stale: 60,
revalidate: 900,
expire: 86400,
},
},
},
};
Stap 2: Zoek uw hot paths
Gebruik uw analytics om de pagina's te identificeren die het meeste verkeer krijgen en waar TTFB er het meest toe doet. Voor ons waren het categoriepagina's (veel verkeer, relatief stabiele inhoud) en productpagina's (veel verkeer, matig dynamische inhoud).
Stap 3: Voeg `"use cache"` top-down toe
Begin met layouts. Als uw root layout navigatiegegevens ophaalt, cache dat eerst -- het is de impact met het hoogste rendement, het laagste risico:
// app/layout.tsx
// Opmerking: "use cache" op layouts cached de layout shell
// Onderliggende pagina's renderen nog steeds onafhankelijk
import { Navigation } from '@/components/Navigation';
export default async function RootLayout({ children }) {
return (
<html>
<body>
<Navigation /> {/* Deze component heeft zijn eigen "use cache" */}
{children}
</body>
</html>
);
}
Stap 4: Stel monitoring in
We gebruikten Vercel's ingebouwde analytics plus aangepast loggen om cache hit-tarieven bij te houden. In de eerste week na het inschakelen van cacheComponents was ons cache hit-tarief slechts 34%. Na het afstemmen van stale duurtijden klom het naar 78%.
Prestatieresultaten en benchmarks
Hier zijn de werkelijke cijfers na de volledige migratie, gemeten over een periode van 30 dagen op Vercel's Pro-plan:
| Meting | 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 |
| Bouwtijd (volledig) | 3u 12m | 2u 48m | 48m |
| Vercel function aanroepen/dag | 2,4M | 3,8M | 1,1M |
| Maandelijkse Vercel-factuur | ~€750 | ~€1.100 | ~€475 |
| Cache hit-tarief | Onbekend (impliciet) | N/A | 78% |
| Verouderde content incidenten (#cache-crimes) | 8-12/week | 0 | 1-2/maand |
De bouwtijdverbetering verdient uitleg. Met cacheComponents verhuisden we weg van het genereren van alle 91.000 pagina's op build-time. In plaats daarvan genereerden we statisch alleen de top 5.000 pagina's (naar verkeer) en lieten de rest on-demand genereren met caching. De cacheComponents directive betekende dat die on-demand pagina's na het eerste bezoek werden gecacht, met cacheLife die staleness controleerde.
De Vercel-factuur daalde significant. Minder function aanroepen (vanwege expliciete component caching) plus kortere bouwtijden betekende echte kostenbesparing. Die ongeveer €290/maand verlaging betaalt zichzelf terug.
Valkuilen en voetangels
Serialisatiegrenzen
De "use cache" directive creëert een serialisatiegren. Alles dat in een gecachte component als props wordt doorgegeven moet serialiseerbaar zijn. We hadden verschillende componenten die callback-functies of React-elementen als props ontvingen -- die braken onmiddellijk. De fix was herstructureren om in plaats daarvan composeerbare patronen te gebruiken:
// ❌ Dit breekt met "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
// onAddToCart is een functie -- niet serialiseerbaar!
}
// ✅ Dit werkt
"use cache";
export async function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
{/* AddToCart is een Client Component, niet gecacht */}
<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 duurtijden.
De `Date.now()` val
Elke component die "use cache" gebruikt en Date.now() of new Date() binnen de gecachte functie aanroept zal die timestamp cachen. We vonden dit in een weergave "laatst bijgewerkt" die hetzelfde moment gedurende uren toonde. De fix: verplaats tijdgevoelige logica naar een Client Component of een niet-gecachte Server Component.
Geneste cache grenzen
Wanneer u gecachte componenten in andere gecachte componenten nest, heeft de binnenste cache zijn eigen levenscyclus. Dit is krachtig maar verwarrend. We hebben een teamafspraak gemaakt: cache op paginaniveau OF op componentniveau, niet beide, tenzij er een duidelijke reden is.
Wanneer u cacheComponents wel en niet moet gebruiken
Gebruik het wanneer:
- U meer dan enkele honderd pagina's heeft en ISR bouwtijden zijn pijnlijk
- Uw inhoud heeft duidelijke versersheidsvereisten die per sectie variëren
- U gedetailleerde controle nodig heeft over wat wordt gecacht vs. altijd-vers
- U draait op Vercel of een platform dat de Next.js cache-lagen ondersteunt
- U wilt infrastructuurkosten op high-traffic sites verminderen
Gebruik het niet wanneer:
- Uw site klein genoeg is dat volledig SSG goed werkt
- Elke pagina is volledig dynamisch (gebruikersspecifieke inhoud overal)
- U niet op een hostingplatform zit dat Next.js caching-infrastructuur ondersteunt
- Uw team nieuw is voor Next.js -- word eerst vertrouwd met de basis
Als u evalueert of uw project dit niveau van cache-controle nodig heeft, of als een ander framework mogelijk een beter fit voor uw inhoudszware site zou zijn, waard het nadenken voordat u zich aan een migratie verbindt.
Voor projecten waarbij inhoud uit meerdere headless CMS-bronnen komt, werkt het cacheTag systeem in Next.js 16 prachtig met headless CMS-architecturen -- elk inhoudstype krijgt zijn eigen ongeldigmakingskanaal.
Veelgestelde vragen
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. Hiermee kunt u expliciet markeren welke componenten moeten worden gecacht en aangepaste cacheprofielen 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 valideert ze opnieuw volgens een schema. cacheComponents laat u individuele componenten binnen een pagina cachen, elk met verschillende cache-lifetimes. Eén pagina kan een header hebben die 24 uur wordt gecacht, productinfo die 1 uur wordt gecacht, en prijzen die nooit worden gecacht. 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 best op Vercel omdat de caching-infrastructuur strak geïntegreerd is. Zelf gehoste Next.js-implementaties kunnen cacheComponents gebruiken met de bestandssysteem cache adapter, maar u krijgt niet de edge distributiecontinu. Platforms als Netlify en Cloudflare voegen ondersteuning toe, maar vanaf mid-2025 blijft Vercel de meest volledige implementatie.
Hoe invalideert ik gecachte componenten in Next.js 16?
U gebruikt cacheTag() in uw gecachte component om tags toe te wijzen, roept vervolgens revalidateTag('tag-name') aan vanuit een server action, route handler of webhook-eindpunt. Dit invalideert alle gecachte componenten met die tag. Het is dezelfde API van Next.js 15, maar het is veel nuttiger nu omdat u expliciete gecachte componenten tagged in plaats van ondoorzichtige framework-level caches.
Zal cacheComponents mijn Vercel-factuur verminderen? Het kan aanzienlijk kosten besparen. In ons geval vielen function aanroepen met 54% omdat gecachte componentresponsen van de cache-laag werden geserveerd in plaats van serverless-functies aan te roepen. De bouwtijdreductie bespaart ook op bouwminuten. Uw ervaring zal variëren op basis van verkeerspatronen en cache hit-tarieven -- controleer Vercel's prijscalculator met uw huidige gebruik.
Wat gebeurt er als ik "use cache" toevoeg aan een component die niet-serialiseerbare props ontvangt?
U krijgt een build-fout. De "use cache" directive creëert een serialisatiegren, dus alle props moeten serialiseerbaar zijn (strings, getallen, platte objecten, arrays). Functies, React-elementen, klasseninstanties en andere niet-serialiseerbare waarden veroorzaken dat de build mislukt. Herstructureer uw component om alleen gegevensprops 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 boven op React's Server Components is gebouwd. Hoewel de "use cache" directive-syntaxis uiteindelijk een React-standaard kan worden, zijn de cacheLife-profielen en cacheTag-systeem Next.js APIs. Als u een framework als Remix of een aangepaste RSC-setup gebruikt, hebt u andere caching-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 10.000 pagina's) met een eenvoudiger gegevensmodel kon het waarschijnlijk in 1-2 weken doen. De werkelijke codewijzigingen zijn eenvoudig -- de tijd gaat naar het controleren van uw caching-behoeften, het testen van ongeldigmakingsstromen en het monitoren van cache hit-tarieven na implementatie.