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.

Leave a Comment