Topic 15 of 47
Caching Strategy
Overview
Next.js has multiple caching layers: Request Memoization, Data Cache, Full Route Cache, and Router Cache. Understanding these prevents stale data and performance issues. Correct cache strategy is critical for production apps.
Syntax
typescript
// 1. Request Memoization — same fetch in same render is deduped
// Both components fetching /api/user share ONE request per render
fetch("/api/user") // component A
fetch("/api/user") // component B — same URL = single request
// 2. Data Cache — persists across requests and deployments
fetch(url, { cache: "force-cache" }); // indefinitely cached (default for GET)
fetch(url, { cache: "no-store" }); // never cache
fetch(url, { next: { revalidate: 60 } }); // cache 60 seconds
// 3. Full Route Cache — static routes cached at build time
export const revalidate = 3600; // revalidate whole route every 1 hour
export const dynamic = "force-dynamic"; // opt out of full route cache
// 4. Router Cache — client-side cache of visited routes
import { useRouter } from "next/navigation";
router.refresh(); // clear router cache for current route
// On-demand revalidation
import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath("/products"); // clear all /products pages
revalidateTag("products"); // clear fetches tagged 'products'Common Pitfalls
- Cookies, headers, and search params make routes dynamic — Next.js automatically opts out of caching when these are accessed.
- Router Cache persists for 30 seconds on user navigation — call router.refresh() to force-refresh current page data.
- Interview tip: ISR (Incremental Static Regeneration) = revalidate: N. Pages stay static but regenerate every N seconds in background. Zero downtime redeployment of content.
Real-World Example
Cache strategy for a blog: static posts with on-demand ISR
example
typescript
// Static blog posts — cached forever, revalidated when author publishes
// app/blog/[slug]/page.tsx
export const revalidate = false; // cache indefinitely
export async function generateStaticParams() {
const posts = await db.posts.findMany({ select: { slug: true } });
return posts.map(p => ({ slug: p.slug }));
}
// Server Action triggered by CMS webhook
export async function revalidatePost(slug: string) {
"use server";
revalidatePath(`/blog/${slug}`); // clear specific post
revalidatePath("/blog"); // clear blog index
revalidatePath("/"); // clear homepage (shows recent posts)
}
// API endpoint for CMS to call
// app/api/revalidate/route.ts
export async function POST(request: Request) {
const body = await request.json();
const secret = request.headers.get("x-webhook-secret");
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: "Invalid secret" }, { status: 401 });
}
revalidatePath(`/blog/${body.slug}`);
return Response.json({ revalidated: true });
}