Next.js Caching
Caching means storing the result of an operation so you can reuse it instead of doing the same work again. When Next.js caches a page or a fetch result, it serves the stored version to the next visitor instead of generating everything from scratch. This makes your app significantly faster.
Why Caching Matters
WITHOUT CACHING: User visits /blog → Database query runs: 200ms → Page renders: 50ms → Total: 250ms per visitor WITH CACHING: First visitor → 250ms (builds and stores result) Next 1000 visitors → 5ms each (served from cache) Database query runs zero additional times
The Four Cache Layers in Next.js
LAYER 1: Request Memoization Scope: Single request lifecycle What: Deduplicates identical fetch calls in one render LAYER 2: Data Cache Scope: Across all requests, persists between deployments What: Stores fetch() results on the server LAYER 3: Full Route Cache Scope: Across all requests, set at build time What: Stores entire pre-rendered pages (HTML + RSC payload) LAYER 4: Router Cache Scope: Browser session What: Stores page data in browser memory for fast back/forward
Layer 1: Request Memoization
When multiple components in the same render tree call fetch with the same URL, Next.js only makes one actual network request. All components get the same result. This means you can fetch data in the component that needs it — without worrying about calling the same API twice.
// Component A calls this:
const user = await fetch('/api/user/42');
// Component B also calls this in the same render:
const user = await fetch('/api/user/42');
// Result: Only ONE network request goes out.
// Both components get the same cached result.
Layer 2: Data Cache
The Data Cache stores fetch results on the Next.js server between requests. Control it with the cache option on each fetch call.
Three Caching Behaviors
// Cache forever (default in production):
fetch(url, { cache: 'force-cache' })
→ Fetched once, stored forever, served to all users
// Never cache:
fetch(url, { cache: 'no-store' })
→ Always fetches fresh data on every request
// Cache with time limit (ISR):
fetch(url, { next: { revalidate: 3600 } })
→ Cached for 1 hour, then refreshed on next request
DIAGRAM: Revalidate Timeline
0s 3600s 7200s
| | |
First request → fetch → cache → serve cached → revalidate → fetch fresh
↑
All requests served from cache
during this window
Layer 3: Full Route Cache
At build time, Next.js pre-renders static pages and stores the complete HTML. These pre-built pages load almost instantly because the server just sends a file — no computation needed at runtime.
STATIC (cached at build): Page uses no dynamic data → Pre-built HTML stored → Every user gets the same file → Extremely fast DYNAMIC (not cached): Page uses cookies, headers, or no-store fetch → Rendered per request → Always fresh data → Slightly slower
Opting Out of Full Route Cache
A page becomes dynamic (not cached) automatically when it uses any of these:
import { cookies } from 'next/headers';
import { headers } from 'next/headers';
// Reading cookies or headers makes the route dynamic:
const token = cookies().get('token');
const agent = headers().get('user-agent');
// Using no-store fetch makes the route dynamic:
fetch(url, { cache: 'no-store' });
// Calling dynamic functions:
const { searchParams } = new URL(request.url);
Revalidating Cached Data
Time-Based Revalidation
Set a revalidation interval on a fetch call or export it from a route segment.
// Option A: Per fetch call
fetch(url, { next: { revalidate: 60 } }) // refresh every 60 seconds
// Option B: Entire route segment
export const revalidate = 60; // all fetches in this page refresh every 60s
On-Demand Revalidation
Trigger a cache refresh manually — after a form submission or webhook — using revalidatePath or revalidateTag.
// After creating a post, refresh the blog page cache:
import { revalidatePath } from 'next/cache';
await db.posts.create({ data: newPost });
revalidatePath('/blog');
// Tag-based revalidation (more targeted):
// Tag your fetch:
fetch(url, { next: { tags: ['posts'] } });
// Then invalidate all fetches with that tag:
import { revalidateTag } from 'next/cache';
revalidateTag('posts');
DIAGRAM: Tag-Based Revalidation
Several pages all fetch posts and tag them 'posts':
/blog/page.js → fetch(..., { next: { tags: ['posts'] } })
/homepage/page.js → fetch(..., { next: { tags: ['posts'] } })
/sitemap.js → fetch(..., { next: { tags: ['posts'] } })
Admin creates a new post → Server Action runs:
revalidateTag('posts')
→ ALL pages tagged 'posts' are refreshed at once
Layer 4: Router Cache (Client-Side)
When you navigate between pages in the browser, Next.js stores the pages you visit in memory. Pressing back or revisiting a page loads it from memory instantly — no server request needed.
User visits /home → data fetched → stored in router cache User visits /about → data fetched → stored in router cache User clicks back to /home → loaded from memory instantly (no fetch)
Disabling Caching for an Entire Route
// Force the entire route to be dynamic (never cached): export const dynamic = 'force-dynamic'; // Force the entire route to be static (always cached): export const dynamic = 'force-static';
Key Takeaway
Next.js caches at four levels: request deduplication, data cache, full route cache, and browser router cache. Control the data cache per fetch using cache: 'force-cache', cache: 'no-store', or next: { revalidate: N }. After data changes, use revalidatePath to refresh a specific page or revalidateTag to refresh all pages that use tagged data. Understanding these layers helps you build apps that are fast and always show fresh data.
