Cached Navigations
The App Shell told the story of the first click — what paints when you navigate somewhere new. This is the sequel: what the router keeps from every page it has already fetched, how long it's allowed to serve it back to you, and how it decides to let go.
Real sessions aren't a single navigation. They're loops: a list, a detail page, back to the list, another detail page, back again. If the router re-downloaded everything on each hop, half of your user's session would be spent re-fetching things their browser was holding a moment ago.
It doesn't. Underneath the App Shell machinery sits a second system — a client-side cache of everything the router has downloaded, and a set of rules for when a navigation may be served from it without asking the server at all. The same two colors from the first guide carry this one: green is served from memory — zero network, effectively instant. Orange goes to the network — it costs a round trip.
The second click
The first navigation to a route is the expensive one. Everything after it is a different question.
Here's the loop to hold in your head for this whole guide. Your user is browsing a store:
/store— the product list. First visit./store/boots— a product page. First visit.- Back to
/store. /store/tote— another product.- Back again.
Five navigations, but only three first visits — and even those three overlap. Every one of them shares the root layout. The two product pages share the store layout and the list page's data-fetching work. Navigations 3 and 5 target a page whose complete rendered result the browser produced seconds ago.
The App Shell guide answered what happens at step 2's click: the shell paints from the prefetch cache, the fresh parts stream. This guide is about the other four-fifths of the session — the part where the router's best move is to not make a request.
Everything in this guide happens in browser memory, inside the
router. It's a different layer from 'use cache''s
server-side entries and from the CDN cache in front of your app.
Those decide how cheaply the server can answer. This layer
decides whether the server is asked at all.
What the router keeps
Per-segment entries, filled by prefetches and navigations, reused across every route that shares them.
Every payload the router downloads — by prefetching a link, or by completing a navigation — is deposited in an in-memory cache, keyed per segment, not per URL. That keying is the detail that makes the whole thing compound. A route is a stack of segments:
/store/boots = root layout · store layout · boots page /store/tote = root layout · store layout · tote page shared: 2 of 3 segments — already cached after the first visit
Navigate from /store/boots to /store/tote
and the router doesn't fetch a "page." It walks the segment stack,
finds the root and store layouts already in memory, and fetches
only the leaf. One small request instead of three. Layouts —
usually the most expensive, most shared parts of your tree — are
fetched roughly once per session, then reused by every sibling
navigation.
The cache is fed from two directions. Navigations deposit what they download on the way through. And the prefetcher fills it speculatively, with a small scheduler deciding what's worth fetching ahead of time:
- links in the viewport are queued first;
- links showing intent — hover, touch — jump the queue;
- newer links replace older ones;
- links scrolled off-screen are discarded.
Everything lands in the same per-segment entries. A prefetch for
/store/tote that arrives while you're reading
/store/boots only downloads the tote leaf — the shared
spine is already there. This is why hundreds of product links don't
mean hundreds of full-page prefetches: most of every destination is
already in memory.
Entries are sized, tracked in an LRU, and evicted under memory pressure — and the whole structure is in-memory, so a hard reload starts from zero. Nothing in this guide is about guaranteeing reuse; it's about what the router does when reuse is possible. Correctness never depends on a cache hit.
Feel it: the lifecycle, sped up
Below is the session loop from chapter 1 as a simulator. Each navigation reports, per segment, whether it was served from memory (HIT) or had to fetch (MISS). The cache table underneath shows each entry aging toward staleness — stale times are sped up to 20 seconds so you can watch a full lifecycle. Chapter 3 explains the aging; chapter 4 the Back button; chapter 5 the two destructive buttons.
Click around the store. Watch which segments fetch, which are served from memory, and what survives Back, a write, and a reload.
| Cached segment | Freshness (20s, sped up) | Status |
|---|
In the simulator, visit /store, then
/store/boots, then /profile. How many
segment fetches did the third navigation need, and why not more?
Reveal answer
One. /profile is root layout + profile page — two
segments — but the root layout has been cached since the very
first navigation. Only the profile leaf fetches. The store layout
stays in memory too, untouched: it's not part of this route, but
it'll be a hit again the moment you go back to the store.
The third number
cacheLife has three fields. Two govern the server's
copy. The one everyone skims past governs the browser's.
The App Shell guide used 'use cache' and
cacheLife() to decide what could be rendered before a
click. Look at a profile again, all three fields this time:
cacheLife({ stale: 300, // browser: serve from memory, no network, for 5 min revalidate: 900, // server: refresh in the background after 15 min expire: 86400, // server: never serve anything older than a day })
revalidate and expire are server policy —
how the shared entry behind 'use cache' ages. But
stale is client policy. When a response
reaches the browser, the server attaches each segment's stale time to
it (an x-nextjs-stale-time response header). From that
moment the router holds a lease: for that many seconds, a navigation
that needs this segment may be answered
entirely from memory. No request. No revalidation
ping. Nothing on the network tab.
When the lease runs out, the entry isn't deleted — it's merely suspect. The next navigation that needs it goes back to the server, and the fresh response resets the clock. In the simulator above, that's the green bar draining: a hit while it's green, a fetch once it empties.
The floor: thirty seconds
One guardrail is easy to miss: the client enforces a minimum stale time of 30 seconds, whatever your profile says. The reason is the prefetcher. A prefetch that expired the instant it landed would be worthless — the user hasn't clicked yet. Thirty seconds is the window that keeps a prefetched link usable between the moment it's fetched and the moment it's clicked.
And recall the advancement rule from the first guide: content with a
very short lifetime — zero revalidate, or
expire under five minutes, like the
seconds profile — never enters a prerender at all. It's
a dynamic hole, streamed after every navigation. The two rules meet
neatly: if it's fresh enough to matter by the second, it was
never in the cache to begin with.
The built-in profiles, third column first
| Profile | stale (browser lease) |
revalidate |
expire |
|---|---|---|---|
default |
5 minutes | 15 minutes | never |
seconds |
30 seconds (the floor) | 1 second | 1 minute |
minutes |
5 minutes | 1 minute | 1 hour |
hours / days / weeks |
5 minutes | hours → 1 hour, days → 1 day, … | longer still |
Notice the shape: the browser lease is deliberately short — minutes, not hours — even for content the server may reuse for weeks. The server copy is shared and revalidated in the background; the browser copy is a snapshot in one tab with no background refresh, so it gets a shorter leash.
A product page's data uses cacheLife('days'). A user
visits it, reads for ten minutes, then clicks a link back to it.
Does that navigation hit the network?
Reveal answer
Yes — one revalidation fetch. The days profile's
browser lease (stale) is 5 minutes, and 10 minutes
have passed, so the client's copy is suspect and the router
checks back with the server. The server's copy is almost
certainly still valid (its revalidate is a day), so
the answer is cheap and probably CDN-served — but it is a network
round trip. If they'd clicked within 5 minutes: zero requests.
Back is different
A fresh navigation asks "is this data still fresh?" History traversal asks a different question entirely.
There are two ways to arrive at a URL you've already seen, and they are not the same operation.
A fresh navigation — clicking a
<Link> to /store — is a request for
the current state of that route. It consults the segment
cache, respects every stale time from chapter 3, and re-fetches
whatever has aged out.
Back and Forward are not requests for the current state. They're requests for where you just were — and the router honors that literally. When a navigation completes, the router keeps the finished page — the rendered result, not just its raw payload — in a back/forward cache. History traversal restores that entry directly, and here's the rule that surprises people: it disregards stale times entirely.
Ten seconds old or ten minutes old, Back restores the page you left. That's not a loophole; it's the contract browsers have always honored for plain documents, carried into the app router. Back means "return me to what I was looking at" — your scroll position, your half-read paragraph — not "re-render this URL and surprise me." Data freshness is a property of moving forward through an app; history is a property of memory.
Fully dynamic, uncached page content — the orange parts — is
not reused for fresh navigations at all. Navigate away
from an uncached dashboard and click a link back to it: the page
renders again, as it must. The back/forward cache is the deliberate
exception — the one place the router will re-show uncached content
without a request, because you're not asking for the route, you're
asking for your own recent past. Try it in the simulator:
/profile is uncached, so visiting it always
fetches — but Back onto it never does.
A user is on /store/boots, and the store layout's
stale time lapsed a minute ago. They press Back to
/store, then immediately click a link to
/store/tote. What does each step cost?
Reveal answer
Back is free — history traversal restores the completed
/store page, stale layout and all, with zero
requests. The click to /store/tote is a fresh
navigation, so staleness applies again: the aged-out store layout
revalidates, and the tote leaf fetches if it isn't cached. Same
destination family, two different questions, two different
prices.
Letting go
Time, memory pressure, writes, and deploys — the four ways entries leave, and why writes take everything with them.
A cache is defined by its exits. This one has four, and you've already met the first two: time (the stale lease from chapter 3 — though a lapsed entry lingers until a navigation replaces it) and pressure (the LRU from chapter 2, evicting cold entries as memory fills). The interesting exits are the other two.
Writes clear everything
When a Server Action calls revalidateTag,
revalidatePath, updateTag, or
refresh — or sets or deletes a cookie — the router
doesn't surgically hunt down affected segments.
It clears the entire client cache, immediately, bypassing
every stale time.
'use server' export async function addToCart(productId: string) { await db.cart.add(productId) updateTag('cart') // server entry refreshed — and the browser cache drops wholesale }
Blunt on purpose. The moment a user writes, every cached snapshot in the tab predates the write — and a UI that shows your new cart item on one page and the old count on another is worse than a slower one. So the guarantee is absolute: after a mutation, nothing stale survives. You never reason about which cached page might still show pre-write data, because none can. This is what makes read-your-own-writes work: the action completes, the cache is empty, and the next paint of anything reflects the world after your write. The prefetcher then quietly refills the entries for the links on screen — the cache rebuilds itself; you don't manage it.
Cookie writes ride the same rule for the same reason: a changed
session means every personalized snapshot — a
'use cache: private' nav bar, a session-keyed shell — is
suspect. Sign out, and the cache holding your name in its shells is
gone before the next frame.
Deploys: the cache that must not survive you shipping
One more exit, easy to forget: a client that has been sitting on a
long-lived tab was built by your previous deploy. Its cached
payloads reference chunks and flight data from that build. Set a
deploymentId in your config, and when the router notices
the server answering from a different deployment, it stops trusting
everything — cache included — and performs a full document
navigation to re-enter the app on the new build. The in-memory cache,
being in-memory, simply doesn't exist on the other side.
In the simulator: fill the cache by visiting all four routes, then
press Server Action · updateTag('products'). The action
only names the products tag — why does the
/profile entry vanish too?
Reveal answer
Because client-side invalidation isn't tag-scoped — the tag governs which server entries refresh. In the browser, any write clears the whole client cache, profile included. The cost is a few re-fetches on the next navigations (the prefetcher largely hides them); the payoff is that no post-write navigation can ever paint pre-write data.
The life of a payload
Five sentences, one lifecycle table.
- Everything the router downloads — prefetches and navigations alike — lands in an in-memory cache keyed per segment, so sibling routes reuse their shared spine and mostly fetch leaves.
-
cacheLife'sstalefield is the browser's lease: within it, navigations needing that segment make no request at all — with a 30-second floor so prefetched links stay usable. - A lapsed lease doesn't delete the entry; it makes the next fresh navigation revalidate it.
- Back and Forward disregard staleness — history traversal restores the completed page you left, because Back means "where I was," not "this URL, current edition."
-
Any write —
revalidateTag,updateTag,refresh, a cookie change — clears the whole client cache at once, so nothing painted after a mutation can predate it.
One entry's life
| Moment | What the router does | Network |
|---|---|---|
| Born | A prefetch or navigation deposits the segment, lease attached | 1 fetch |
Fresh (within stale) |
Every navigation needing it is served from memory | zero |
| Lapsed | Still present; next fresh navigation revalidates it | 1 fetch, clock resets |
| Back / Forward | Completed page restored regardless of lease | zero |
| After a write | Gone — with everything else, immediately | refetch on next use |
| Cold / reload / new deploy | Evicted by LRU, or the whole in-memory cache ceases to exist | refetch on next use |
Further reading
In the Next.js docs: Prefetching (guides — the client cache
and scheduler), the `cacheLife` API reference (the
stale field and client cache behavior), the
Glossary entry for Client Cache, and
How revalidation works (guides — the server side of the
story, and why HTML and RSC payloads are cached together). And the
prequel to this guide:
The App Shell — Instant Navigations
with Cache Components.