Next.js Caching Strategies
Answer
Next.js has multiple caching layers to optimize performance. Understanding how they work together is crucial for building fast applications.
Caching Layers
1. Request Memoization
// Same fetch in multiple components - only one request
async function getUser(id) {
// Memoized within same render
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// Both components fetch, but only 1 actual request
async function Sidebar() {
const user = await getUser(1);
return <div>{user.name}</div>;
}
async function Header() {
const user = await getUser(1); // Deduped!
return <div>Welcome, {user.name}</div>;
}
2. Data Cache
// Cached persistently (across requests)
const data = await fetch("https://api.example.com/data", {
cache: "force-cache", // Default - cached indefinitely
});
// No cache - always fresh
const data = await fetch("https://api.example.com/data", {
cache: "no-store",
});
// Time-based revalidation
const data = await fetch("https://api.example.com/data", {
next: { revalidate: 60 }, // Cache for 60 seconds
});
// Tag-based cache
const data = await fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
// Revalidate by tag
import { revalidateTag } from "next/cache";
revalidateTag("posts");
3. Full Route Cache
// Static routes are cached at build time
// app/about/page.js
export default function About() {
return <div>About Us</div>;
}
// Cached as static HTML
// Dynamic routes opt out
export const dynamic = 'force-dynamic';
// Or use dynamic functions
import { cookies, headers } from 'next/headers';
export default function Page() {
const cookieStore = cookies(); // Makes route dynamic
return <div>...</div>;
}
4. Router Cache (Client-side)
// Prefetched routes cached in browser
<Link href="/about">About</Link>; // Prefetched, cached
// Invalidate router cache
import { useRouter } from "next/navigation";
const router = useRouter();
router.refresh(); // Invalidates router cache
Cache Configuration
// Route Segment Config
export const dynamic = "auto"; // Default
export const dynamic = "force-dynamic"; // Always SSR
export const dynamic = "force-static"; // Always SSG
export const dynamic = "error"; // Error if dynamic
export const revalidate = 60; // ISR every 60s
export const revalidate = 0; // Always SSR
export const revalidate = false; // Cache forever
export const fetchCache = "auto";
export const fetchCache = "default-cache";
export const fetchCache = "only-cache";
export const fetchCache = "force-no-store";
export const fetchCache = "default-no-store";
Opting Out of Caching
// 1. Using dynamic functions
import { cookies, headers } from "next/headers";
// 2. Route config
export const dynamic = "force-dynamic";
// 3. Per-fetch
fetch(url, { cache: "no-store" });
// 4. unstable_noStore
import { unstable_noStore as noStore } from "next/cache";
async function Component() {
noStore(); // Opts out of caching
const data = await fetchData();
}
On-Demand Revalidation
// API Route to trigger revalidation
// app/api/revalidate/route.js
import { revalidatePath, revalidateTag } from "next/cache";
export async function POST(request) {
const { path, tag, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: "Invalid secret" }, { status: 401 });
}
if (path) {
revalidatePath(path);
}
if (tag) {
revalidateTag(tag);
}
return Response.json({ revalidated: true, now: Date.now() });
}
// Call from CMS webhook
// POST /api/revalidate
// { "tag": "posts", "secret": "..." }
Cache Hierarchy
| Cache | Duration | Scope | Opt Out |
|---|---|---|---|
| Request Memo | Per request | Server | N/A |
| Data Cache | Persistent | Server | no-store, revalidate: 0 |
| Full Route | Build time | Server | force-dynamic |
| Router Cache | Session | Client | router.refresh() |
Debug Caching
// Check cache status in headers
// x-nextjs-cache: HIT | MISS | STALE
// Log fetch cache status
const res = await fetch(url);
console.log("Cache:", res.headers.get("x-nextjs-cache"));
Key Points
- Request Memoization: Dedupes within single render
- Data Cache: Persistent, revalidate with time or tags
- Full Route Cache: Static pages cached at build
- Router Cache: Client-side navigation cache
- Use
revalidatePath/revalidateTagfor on-demand updates force-dynamicopts out of full route cachecache: 'no-store'opts out of data cache