← Jude Gao Cached Navigations
Chapters ▾
A Next.js field guide · Cache Components

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.

Chapter 1

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:

  1. /store — the product list. First visit.
  2. /store/boots — a product page. First visit.
  3. Back to /store.
  4. /store/tote — another product.
  5. 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.

Where this system lives

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.

Chapter 2

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:

two routes, one shared spine
/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:

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.

It's a cache, not a promise

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
— no navigations yet. Visit /store to begin. —
Check yourself

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.

Chapter 3

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:

a cacheLife profile
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.

Check yourself

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.

Chapter 4

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.

Why this matters for dynamic pages

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.

Check yourself

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.

Chapter 5

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.

app/actions.ts
'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.

Check yourself

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.

Recap

The life of a payload

Five sentences, one lifecycle table.

  1. 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.
  2. cacheLife's stale field 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.
  3. A lapsed lease doesn't delete the entry; it makes the next fresh navigation revalidate it.
  4. Back and Forward disregard staleness — history traversal restores the completed page you left, because Back means "where I was," not "this URL, current edition."
  5. 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.