Next.js 15 introduces a modern approach to caching built around server components, static rendering, and fine-grained control over dynamic behavior. Whether you're building a high-performance landing page, a real-time dashboard, or a hybrid app with both static and dynamic data, Next.js gives you powerful tools to manage what gets cached, when, and for how long.
This tutorial provides a hands-on walkthrough of the different caching strategies available in Next.js 15 using the App Router. Each example demonstrates a core concept in isolation so you can see how caching affects rendering behavior, performance, and data freshness.
Use the links below to explore:
fetch()Whether you're new to caching or looking to deepen your understanding of how Next.js handles it under the hood, this guide will give you the foundation to make smart, performant decisions in your own projects.
Install a new Next.js 15 project:
npx create-next-app@latest next15-cache-demo
cd next15-cache-demo
Make sure you select the App Router during setup.
Next.js 15 supports three main cache modes for server-rendered pages and data:
| Setting | Behavior | Use Case |
|---|---|---|
dynamic = 'force-static' | Fully static | Blog pages, landing pages |
revalidate = 60 | ISR (cache for 60s) | News, product pages |
dynamic = 'force-dynamic' | No cache | Dashboards, auth pages |
dynamic = 'force-static'In app/static-example/page.tsx:
export const dynamic = "force-static";
export default function StaticPage() {
const now = new Date().toISOString();
return <div>This page was rendered at {now}</div>;
}
Now visit /static-example — the page is built once and cached permanently until you redeploy.
revalidate = 10In app/revalidated-example/page.tsx:
export const revalidate = 10;
export default function RevalidatedPage() {
const now = new Date().toISOString();
return <div>This page was rendered at {now} and revalidates every 10s.</div>;
}
Visit /revalidated-example and refresh. You'll see the timestamp update every 10 seconds.
In app/dynamic-example/page.tsx:
export const dynamic = "force-dynamic";
export default function DynamicPage() {
const now = new Date().toISOString();
return <div>This page renders fresh every time: {now}</div>;
}
Visit /dynamic-example and see how it updates every time — no caching at all.
fetchIn app/api-example/page.tsx:
export default async function ApiExample() {
const res = await fetch("https://worldtimeapi.org/api/ip", {
next: { revalidate: 15 }, // cache this response for 15s
});
const data = await res.json();
return (
<div>
<p>Current time: {data.datetime}</p>
<p>Data is cached and refreshed every 15 seconds.</p>
</div>
);
}
You can swap revalidate with cache: 'no-store' for real-time data.
revalidate instead of dynamic = 'force-dynamic' whenever possibleThis allows for stale-while-revalidate, which is faster and more scalable.
Always use next: { revalidate } or cache: 'no-store' with fetch() to control behavior.
Avoid caching user-specific data or authenticated content.
Example: a dashboard layout with dynamic = 'force-dynamic' ensures all child pages inherit that behavior.
When setting up caching:
revalidate: 60)dynamic = 'force-dynamic'fetch(..., { next: { revalidate } })Set up an API route like /api/revalidate:
// app/api/revalidate/route.ts
export async function POST(request: Request) {
const { path } = await request.json();
await fetch(`https://your-site.com${path}`, {
method: "PURGE", // Simulated
});
return NextResponse.json({ revalidated: true });
}
This simulates an external CMS calling to clear cache.