Skip to main content

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

CacheDurationScopeOpt Out
Request MemoPer requestServerN/A
Data CachePersistentServerno-store, revalidate: 0
Full RouteBuild timeServerforce-dynamic
Router CacheSessionClientrouter.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 / revalidateTag for on-demand updates
  • force-dynamic opts out of full route cache
  • cache: 'no-store' opts out of data cache