Caching in NextJS made simple
NextJS does some automatic caching for you; some of it is is opt-in and some opt-out. To understand it, you’d have to read the docs, but some of it is hard to grasp. Well, this article aims at simplifying some of that and breaking it down so it’s more easily understood.
Intro
NextJS offers 4 different mechanisms of caching:
- Request Memoization (Data)
- Data Cache (Data)
- Full Route Cache (UI)
- Client-Side Router Cache (UI)
The last one is the only one living on the Client, while the rest live on the Server.
Request Memoization (Deduplication)
NextJS can dedupe requests to the same resource within the same render cycle (NextJS server request). If you make a fetch request in a layout
file and then make the same request in a page
file below it, NextJS will only make 1 external request to fetch your resource.
A few things to note:
- This cache is active for a single render cycle & resets on every new request to the NextJS server.
- It’s only purpose is to avoid duplicate fetching.
- Caching happens automatically on
/GET
requests when using the native NodeJSfetch()
up until Next14. It’s opt-in from Next15 and on. - If you’re not using the native
fetch
(e.g. you are usingaxios
), you can opt-in to this behaviour by wrapping your fetch operation with thecache()
function that React offers:
import { cache } from 'react'
const fetchResource = cache(() => axios.get(`https://...`))
Data Cache
While the first cache resets on every new server request, this cache is there to persist accross multiple requests made by different users in your app. It even persists between different deployments.
Until NextJS 14, this kind of caching was applied by default whenever your request didn’t depend on changing headers and cookies. From NextJS 15 and on, this behaviour is opt-in.
If you’re using the native fetch
, you can opt-in (or opt-out in versions earlier than NextJS 15 ) by changing the cache
option:
fetch(`https://...`, { cache: 'force-cache' })
If you’re not using the native fetch
, you can opt-in to this behaviour by wrapping your request with the unstable_cache
function:
import { unstable_cache } from 'next/cache'
const fetchResource = unstable_cache(() => axios.get(`https://...`))
You can invalidate it either via time (via a stale-while-revalidate strategy):
fetch('https://...', { next: { revalidate: 3600 } })
// or if not using native `fetch`
unstable_cache(() => axios.get(`https://...`), ['CACHE_KEY'], { revalidate: 3600 })
Or manually by adding a tag to each cache entry and calling revalidateTag
:
fetch(`https://...`, { next: { tags: ['foo'] } })
// or if not using native `fetch`
unstable_cache(() => axios.get(`https://...`), ['CACHE_KEY'], { tags: ['foo'] })
// and then
import { revalidateTag } from 'next/cache'
revalidateTag('foo')
A few notes around this cache:
- This cache is for data only and has nothing to do with UI and React.
- Manual invalidation can only happen within Server Actions or Route Handlers.
- Data cache entries don’t automatically get invalidated if the pages using those entries get invalidated (more on that later).
Full Route Cache
The only thing this cache does is to store UI pages that have evaluated during build-time. This is where pages that are statically rendered are stored, so they are immediately available.
You can invalidate the built-time pages stored in this cache either via time:
// static page available under {host}/random
const Page = () => {
return <div>{Math.random()}</div>
}
export default Page
export const revalidate = 3600 // rebuild this page every hour
or manually through the page’s URL:
import { revalidatePath } from 'next/cache'
revalidatePath('/random')
A few notes around this cache:
- NextJS decides when a page is dynamically (at request time) or statically rendered (at build time) based on whether it depends on request-time data or not. Only statically rendered pages are added to this cache.
- If a page doesn’t depend on any request-time data, it’s most likely statically rendered.
- If a page depends on request-time data (e.g. headers, cookies, etc.) and this data is cached in the Data Cache (either
GET
requests viafetch
on Next 13-14 or functions wrapped withunstable_cache
), then the page is statically rendered. - If a page contains request-time data that are cached for 10 seconds and this page gets revalidated every 20 seconds, then the page will always revalidate every 10 seconds, ignoring your 20 seconds revalidation.
- If a page doesn’t depend on any request-time data, but its parent
layout
does, then this page won’t be statically rendered.
As you see, there are a lot of gotchas when trying to aim for static builds. The easiest way to know if a page will be cached or not is to run next build
and let NextJS tell you via the console. From Next15 and on, there’s also a static indicator that’ll show you an icon in the bottom of the page during development.
Client-Side Router Cache
This is the only cache living on the client and it stores Pages rendered on the server (via RSCs). Server-rendered pages get cached for a few seconds to avoid flooding the server with requests to render the same thing for the same user.
This has a few gotchas, since you might expect that every request to a page that’s dynamically rendered on the server will always execute new requests and fetch fresh data, while this won’t always happen.
Before NextJS 15, every page that was dynamically rendered was, by default, cached on the Client for 30 seconds. From Next 15 and on, no page is cached unless you explicitly tell NextJS to do so.
This is controlled via next.config.js
:
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30,
static: 0,
},
},
};
export default nextConfig
This lets the client-side cache know how long to cache dynamically and statically rendered pages. If there’s a cache-hit on the client-side cache, no request is sent to the server at all.
On NextJS 15, nothing is cached by default. If you opt in to client-side caching, you can refresh stuff manually either within a React component via router.refresh()
:
import router from 'next/navigation';
const Component = () => {
const router = useRouter()
return (
<button onClick={() => router.refresh()}>
recalculate page component
</button>
}
or through a server-action / route handler by calling revalidatePath
or revalidateTag
:
import { revalidatePath } from 'next/cache'
revalidatePath('/my-page-path')
A few things to note:
- Calling
router.refresh()
recalculates the page without any invalidations. CallingrevalidatePath
invalidates related entries in both the Data Cache and the Full Route Cache. - Updating cookies will implicitly purge the client-side cache entry for this page as well
- No matter if you opt-in or opt-out, the client-side Cache will still be used for backwards/forwards navigation in the browser (when the user clicks back/forward). This is so that back/forward navigations are instant.
Final Notes
That’s pretty much it.
- Request Memoization deduplicates requests within a single render
- Data Cache stores results and uses them among different requests and users
- Full Route Cache stores whatever is statically built on the server
- Router Cache stores server-rendered pages to avoid hitting the server too many times. Before NextJS 15 you need to remember that this happens implicitly for dynamically rendered pages, which are stored for 30 seconds on the client.
Let me know your thoughts or whether I forgot to mention something. Thanks for reading!
P.S. 👋 Hi, I’m Aggelos! If you liked this, consider following me on medium 😀