An in-depth look at caching in Next.js, explaining the history of caching and the modern caching strategy with Partial Prerendering and cache components.
Listen to article0:00 / 0:00
What is caching? (a brief overview)
Caching is the process of storing copies of frequently accessed data in a fast, temporary storage layer (a cache), so that future requests can serve that data much faster than fetching it from the primary source.
With caching (fast)
Without caching (slow)
Many webpage resources rarely change, so storing them in fast temporary memory avoids slow, repeated database queries:
The evolution of caching in Next.js:
To understand the core of how caching works in Next.js, it is first important to understand a bit of historical context.
This builds the basis for understanding why caching works the way it does today.
Next.js 14
Next.js 14 cached aggressively by default.
fetch was cached by default (force-cache).
Every page was static and cached by default unless you used one of these three functions (that made the route dynamic):
cookies()
headers()
searchParams()
If a user clicked a <Link>, Next.js stored the React Server Component (RSC) payload directly inside the user's browser memory.
Static routes: Cached in the browser for 5 minutes.
Dynamic routes: Cached in the browser for 30 seconds.
Even if your backend server had fresh data, a user navigating between pages within the 30 second to 5 minute window would see old, cached UI from their browsers memory without a server request being made.
GET route handlers were statically cached at build time by default, unless you explicitly opted out with dynamic functions (like cookies()), or added export const dynamic = 'force-dynamic'.
An endpoint returning dynamic data like Response.json({ time: Date.now() }) would serve the exact same frozen timestamp to every user.
Why they did it:
They wanted apps to be blazingly fast out of the box by generating static pages whenever possible.
Why it backfired:
Unexpected stale data: Developers would fetch something dynamic (like a user profile, stock price, or real-time feed), and it would render once at build time and never update unless manually revalidated.
Confusion with standard JS: In standard JavaScript, fetch() goes to the network every single time. Next.js overriding this standard web behavior caused endless debugging sessions.
Opt-out fatigue: Developers had to explicitly write { cache: 'no-store' } on almost every fetch call just to get normal HTTP behavior.
Next.js 15+:
To address the aforementioned issues, Next.js 15 adopted an uncached by default model which persists to this day.
fetch requests became uncached by default (no-store), with data being fetched fresh on every request unless you explicitly add { cache: 'force-cache' } or set next: { revalidate: X }.
GET route handlers became uncached by default - to make a route handler static, you now have to write export const dynamic = 'force-static'.
The default staleTime for dynamic components is now set to 0 seconds. Meaning that navigating between pages re-fetches the latest data from the server by default.
Runtime request-specific APIs (like cookies(), headers(), params and searchParams()) were turned into asynchronous promises.
Next.js 15 introduced the experimental 'use cache' directive to give the developer greater, explicit, fine-grained control over what should be cached. This would be later refined in Next.js 16.
Next.js 16 and the introduction of cache components
Cache components were introduced as an experimental feature in Next.js 15 with the 'use cache' directive.
It allowed developers to opt individual components, functions, or files into caching while keeping the rest of the application dynamic by default.
In Next.js 16, 'use cache' was refined and unified with Partial Prerendering (PPR).
To understand cache components, it is first important to understand what Partial Prerendering is.
Partial prerendering (PPR)
From the Next.js documentation:
"Partial Prerendering (PPR) combines static and dynamic rendering in a single route. At build time, Next.js generates a static HTML shell and a postponedState blob for each PPR-enabled route. At request time, the shell is served immediately and dynamic portions are rendered and streamed to the client."
The general idea being that instead of opting an entire route into static or dynamic rendering, with PPR, you can make different components static or dynamic within the same route (you don't have to choose one or the other).
With PPR, you get the benefits of static rendering (improved performance, content served instantly from a CDN) and dynamic rendering (real-time data feeds) on the same route.
Build time: at build time, the static shell is prerendered. Where dynamic components sit inside a <Suspense> fallback, Next.js leaves a lightweight placeholder slot.
Request time: When a user requests the page, the server instantly sends the prebuilt static shell. The browser renders the layout and fallback UI (loading skeletons) instantly.
Streaming phase: in the background of that same connection, the server executes the async dynamic components and streams the HTML chunks down to fill the placeholder slots as they complete.
Example
app/page.tsx
import { Suspense } from'react';
import { StaticHeader } from'@/components/Header';
import { ProductDetails } from'@/components/ProductDetails';
import { CartBadge, CartSkeleton } from'@/components/Cart';
import { RecommendedProducts, RecsSkeleton } from'@/components/Recs';
exportdefaultfunctionPage() {
return (
<main>
{/* 1. Static: Included in the initial static HTML shell */}
<StaticHeader /><ProductDetails />
{/* 2. Dynamic: Suspended boundary turned into a dynamic stream slot */}
<Suspensefallback={<CartSkeleton />}>
<CartBadge /> {/* Reads cookies / user session */}
</Suspense>
{/* 3. Dynamic: Streams independently from the cart */}
<Suspensefallback={<RecsSkeleton />}>
<RecommendedProducts /> {/* Slow database query */}
</Suspense></main>
);
}
Cache components
Partial Prerendering was initially introduced as a rendering model to solve the problem of routes needing to be 100% static or 100% dynamic.
Cache components were later introduced to unify Next.js's caching strategy with this model.
Cache components are the primary way that Next.js caching works today.
To enable cache components, set the cacheComponents flag to true in your applications next.config.ts file:
You can cache the rendered output of a server component. If the props do not change, Next.js skips running the component logic and serves the cached result.
'use cache' can be used at the top of a file to indicate that all exports in the file should be cached, or inline at the top of a function or component to cache the return value.
Functions and components that use 'use cache' should always be async.
The cacheLife function with use cache
The cacheLife function is used to set the cache lifetime of a function or component.
It should be used in conjunction with the 'use cache' directive, and within the scope of the function or component.
Essentially, it is used alongside 'use cache' to determine how long a specific function, component or route should stay cached for and when it should revalidate.
cacheLife has three different timers:
stale (stale while revalidate): How long the cached response is considered fresh. During this time, users get the cached response immediately but data may be outdated.
revalidate: The window where Next.js continues to serve the stale cache while silently fetching fresh data in the background.
expire: The maximum time an entry can sit unused in cache before it is discarded and forces a fresh request on the next visit.
Next.js has different cacheLife profiles so you don't have to remember exact second counts:
Comments (0)