
All the rendering, streaming, and prefetching optimizations would be wasted without a caching strategy. Next.js has a multi-layered cache system. Understanding when data is cached versus when it hits the server is essential for production performance.
Not all pages are rendered the same way. Next.js operates on a spectrum:
Static ←────────────────────────────────────────→ Dynamic
(Build time) (Request time)
Static: generateStaticParams → pre-rendered at build → cached indefinitely
ISR: revalidate = N → pre-rendered, refreshed every N seconds
Dynamic: depends on request-time data → rendered per request
Forced Dyn: export const dynamic = 'force-dynamic'A page's position on this spectrum is determined by what it accesses:
cookies(), headers(), or searchParams → DynamicgenerateStaticParams with revalidate → ISRgenerateStaticParams without revalidate → Staticexport const dynamic = 'force-static' → StaticISR is the sweet spot: pre-render at build time, but refresh periodically without redeploying.
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Rebuild no more than once per hour
export async function generateStaticParams() {
const posts = await getAllPublishedPosts()
return posts.map(post => ({ slug: post.slug }))
}
export default async function PostPage({ params }) {
const { slug } = await params
const post = await getPost(slug)
return <Article post={post} />
}The ISR lifecycle:
t=0 (build): All known slugs pre-rendered as static HTML → cached
t=0-3600s: All requests served from cache (instant)
t=3600s: Next request triggers background revalidation
Current request still gets the stale (cached) version
Background: regenerate, swap cache atomically
t=3601s+: Next request gets the fresh versionThe stale-while-revalidate behavior means users never wait for regeneration — they always get the cached version, and the cache is updated atomically in the background.
Beyond time-based ISR, you can revalidate specific pages on data changes:
// After updating a blog post in the CMS...
import { revalidatePath, revalidateTag } from 'next/cache'
// Revalidate a specific page
revalidatePath('/blog/hello-world')
// Revalidate an entire segment
revalidatePath('/blog')
// Revalidate by tag
revalidateTag('blog-posts')Tags are set during data fetching:
// In your data fetching function
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['blog-posts'] }
})This is how headless CMS integrations (Contentful, Sanity, etc.) keep Next.js pages in sync — a webhook fires revalidateTag whenever content changes.
React's cache() function deduplicates function calls within a single request:
import { cache } from 'react'
export const getPost = cache(async (slug: string) => {
return await db.post.findUnique({ where: { slug } })
})
// In generateMetadata and page.tsx — only one DB query executes
export async function generateMetadata({ params }) {
const post = await getPost(params.slug) // First call
return { title: post.title }
}
export default async function Page({ params }) {
const post = await getPost(params.slug) // Returns cached result — no second query
return <Article post={post} />
}
cache()is per-request, not global. Each HTTP request gets its own cache instance. It's designed to prevent duplicate work within a single render pass.
Next.js maintains an in-memory cache of RSC Payloads on the client. When navigating between sibling routes (e.g., /dashboard/settings → /dashboard/analytics), shared layout data is reused — only the changed page segment is fetched:
// Navigation from /dashboard/settings to /dashboard/analytics
// The /dashboard layout RSC Payload is already in the client cache
// Only /dashboard/analytics page data is fetchedConfigure via staleTimes in next.config.js:
// next.config.js
module.exports = {
staleTimes: {
dynamic: 30, // Dynamic routes: cache for 30s
static: 300, // Static routes: cache for 5 min
}
}┌─────────────────────────────────────────────────────────┐
│ Request Arrives │
└──────────────────────┬──────────────────────────────────┘
↓
┌────────────────┐
│ Static Page? │──Yes──→ Serve from CDN/Build Cache
└───────┬────────┘
│ No
↓
┌────────────────┐
│ ISR Page? │──Yes──→ Serve cached version
│ (revalidate=N) │ Trigger background revalidation
└───────┬────────┘
│
↓
┌────────────────┐
│ Dynamic Page? │──→ Render on server (cookies/headers/sear
│ │ chParams triggered)
└───────┬────────┘
↓
┌─────────────────┐
│ cache() dedup │ ← Per-request deduplication
│ within request │
└────────┬────────┘
↓
┌─────────────────┐
│ RSC Payload → │ ← Returned to client
│ Client Cache │ (staleTimes TTL)
└─────────────────┘revalidate + generateStaticParams — pre-render at build, refresh on schedule.revalidatePath and revalidateTag enable on-demand cache invalidation.cache() deduplicates within a request — prevent duplicate DB queries in the same render.Previous:
This concludes the Understanding Next.js series. Return to the Series Guide for the full reading roadmap.