GAP·MAP

Next.js rendering & caching

[ middle depth ]

Static by default, dynamic when you touch a dynamic API

An App Router route is prerendered at build time unless something forces it to render per request. You do not pick static or dynamic; Next infers it from the APIs you call. Reading a dynamic API (cookies(), headers(), draftMode(), or the searchParams page prop, all async in Next 15) switches the entire route to dynamic. The params prop is also async in Next 15, but reading it does not force dynamic rendering; it resolves statically at build through generateStaticParams. So does a single fetch(url, { cache: 'no-store' }) or the config export const dynamic = 'force-dynamic'.

The rule people miss: one dynamic API taints the whole route, not the component that called it. A lone await cookies() in a nested server component makes the whole page render per request.

The four caches and where each one lives

Next stacks four independent caches along the path from click to database:

CacheStoresWhereLifetime
Request Memoizationfetch return valuesserverone render pass
Data Cachefetch/data resultsserverpersistent, survives redeploys
Full Route CacheHTML + RSC payloadserverpersistent, cleared on redeploy
Router CacheRSC payload per segmentbrowsersession, cleared on refresh

Request Memoization is a React feature: identical GET fetches in a single render are deduped, so you can fetch the same data in a layout and a page without prop drilling. The other three persist between requests.

01Router Cacheclient, in-memory, per session02Full Route Cacheserver, prebuilt RSC+HTML03renderReact renders RSC to HTML04Request Memoizationdedupe fetches in this pass05Data Cacheserver, persists across deploys06origindatabase or API - source of truth
fig · A request through the four caches

fetch is no longer cached by default in Next 15

Wrong "A plain fetch in a server component is cached, so it only hits my API once." That was the Next 14 default (force-cache). Next 15 defaults to auto no cache, and the result is not persisted in the Data Cache.

Right opt in explicitly. { cache: 'force-cache' } stores it in the Data Cache; { next: { revalidate: 3600 } } caches it with a one-hour stale-while-revalidate window; { next: { tags: ['products'] } } makes it revalidatable on demand.

Revalidation: time-based and on-demand

Time-based revalidation (revalidate: n, the ISR pattern) serves cached data inside the window; the first request after serves stale and refreshes in the background. On-demand revalidation runs revalidateTag(tag) or revalidatePath(path) from a Server Action after a mutation, purging the Data Cache and Full Route Cache. Call them in an Action or Route Handler, never during render.

[ senior depth ]

The Next 14 to 15 caching-default flip

Most caching confusion traces to defaults that changed in Next 15:

BehaviorNext 14Next 15
fetchcached (force-cache)not cached (auto no cache)
GET Route Handlercached by defaultnot cached; opt in with force-static
Router Cache pages30s dynamic / 5min staticpage staleTime: 0; loading.js still 5min
cookies/headers/params/searchParamssyncasync, must be awaited

A GET Route Handler sits outside the React tree, so it gets no Request Memoization and, since Next 15, no Data Cache by default. Add export const dynamic = 'force-static' to cache one; metadata handlers (sitemap.ts, opengraph-image.tsx) stay static.

Uncached is not no-store

A default (auto no cache) fetch and an explicit no-store fetch diverge on a static route. The default runs once at next build, bakes into the Full Route Cache, and is not persisted in the Data Cache. no-store is stronger: it opts the route out of the Full Route Cache, so the route turns dynamic and the fetch runs every request. "Uncached" does not imply "runs at request time" unless the route is already dynamic. export const dynamic = 'force-dynamic' also flips every fetch default to no-store.

Which invalidation reaches which cache

  • revalidatePath / revalidateTag purge the Data Cache and Full Route Cache; called in a Server Action they also invalidate the client Router Cache.
  • The same calls in a Route Handler bust the server caches but leave the Router Cache stale, because a handler is not tied to a route; the browser serves the old page until a hard refresh or auto-invalidation.
  • router.refresh() clears only the Router Cache, then re-renders the route on the server, which still reads unchanged values from the Data Cache.

Both functions throw if called during render in Next 15; they belong in Actions and handlers. The lowest revalidate across a route's layouts and pages sets the window for the whole route.

Suspense does not make a component dynamic

Streaming is on by default: loading.tsx wraps the segment below it in a <Suspense> boundary whose fallback streams first, then the resolved content swaps in. Wrapping a component in <Suspense> does not opt it into dynamic rendering; a component doing only synchronous work still prerenders inside the boundary. Streaming governs when chunks arrive, not whether the route is static. You also cannot fully opt out of the Router Cache: <Link prefetch={false}> disables prefetch, yet a visited route is still cached for the session. The forward-looking model (Next 16 Cache Components with use cache) replaces these implicit defaults with explicit opt-in caching.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more Next.js

was this useful?