2025-05-23 Web Development
Caching in Next.js 15 – A Developer's Guide
By O. Wolfson
Introduction: Understanding Caching in Next.js 15
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:
- Fully static rendering
- Incremental Static Regeneration (ISR)
- Fully dynamic, uncached pages
- API response caching with
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.
1. Getting Started with a Fresh App
Install a new Next.js 15 project:
bashnpx create-next-app@latest next15-cache-demo
cd next15-cache-demo
Make sure you select the App Router during setup.
2. Understand Caching Modes
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 |
3. Demo: Static Page with dynamic = 'force-static'
In app/static-example/page.tsx
:
tsxexport 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.
4. Demo: Revalidated Page with revalidate = 10
In app/revalidated-example/page.tsx
:
tsxexport 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.
5. Demo: Fully Dynamic Page
In app/dynamic-example/page.tsx
:
tsxexport 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.
6. Demo: Caching API Data with fetch
In app/api-example/page.tsx
:
tsxexport 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.
7. Best Practices
🔹 Use revalidate
instead of dynamic = 'force-dynamic'
whenever possible
This allows for stale-while-revalidate, which is faster and more scalable.
🔹 Cache external API responses explicitly
Always use next: { revalidate }
or cache: 'no-store'
with fetch()
to control behavior.
🔹 Don’t over-cache dynamic data
Avoid caching user-specific data or authenticated content.
🔹 Group caching by layout if consistent
Example: a dashboard layout with dynamic = 'force-dynamic'
ensures all child pages inherit that behavior.
8. Standard Procedure for New Projects
When setting up caching:
- Default pages: use static or ISR (
revalidate: 60
) - Dynamic dashboards: use
dynamic = 'force-dynamic'
- API responses: use
fetch(..., { next: { revalidate } })
- Authenticated routes: disable caching entirely
Bonus: Manual Cache Revalidation
Set up an API route like /api/revalidate
:
ts// app/api/revalidate/route.ts
import { NextResponse } from "next/server";
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.