Next.js App Router Rendering Choices — Per-Route Control
TL;DR
Next.js App Router lets you set rendering behavior per route segment via exported constants and routeRules. The senior knowledge: how to opt routes into static, dynamic, ISR, edge, partial pre-rendering (PPR); how the fetch cache interacts; caching directives (cache:, next.revalidate, next.tags); and how Next 15+ changed defaults (caching is now opt-in, not opt-out).
Interview Q&A
Q: How do you set a route’s rendering mode?
A: Export constants from page.tsx/layout.tsx/route.ts:
// app/posts/[id]/page.tsx
export const dynamic = "force-static"; // or "force-dynamic", "auto", "error"
export const revalidate = 60; // ISR — seconds between regenerations
export const runtime = "edge"; // or "nodejs" (default)
export const fetchCache = "default-cache"; // fine-grained cache control
export const dynamicParams = true; // allow on-demand SSG of new params
What each does:
| Export | Effect |
|---|---|
dynamic = "auto" (default) |
Next decides based on what your code does (uses cookies(), headers(), etc → dynamic) |
dynamic = "force-dynamic" |
always rendered per-request (SSR) |
dynamic = "force-static" |
always rendered at build (SSG) |
dynamic = "error" |
throw if a dynamic operation is detected (catches accidental SSR) |
revalidate = N |
ISR — at most one regeneration per N seconds |
revalidate = false (default) |
no ISR — pure static (until next build) |
runtime = "edge" |
run on edge runtime (V8 isolate, faster cold start, limited APIs) |
runtime = "nodejs" |
full Node runtime (default) |
Q: What triggers “dynamic” rendering automatically?
A: Next.js renders the route dynamically (per-request) if your code uses:
cookies()— reading cookies (per-user).headers()— reading request headers (per-request).searchParamsprop (in App Router) — depends on URL.fetch(url, { cache: "no-store" })— explicit non-cached fetch.- Server Actions (rendering after a mutation).
Once any of these is detected, the route can’t be pre-rendered statically — it’s per-request SSR.
This is why a route that should be static may surprise you with “this route is dynamic” — usually a hidden cookies() call in a deep dependency.
Q: ISR — show me.
A:
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // 1 hour
export async function generateStaticParams() {
const posts = await db.posts.findMany({ select: { slug: true } });
return posts.map(p => ({ slug: p.slug })); // pre-render at build
}
export default async function Post({ params }: { params: { slug: string } }) {
const post = await db.posts.findUnique({ where: { slug: params.slug } });
return <Article post={post} />;
}
Behavior:
- At build: pre-render all posts from
generateStaticParams. - First request after deploy: serve static.
- Within 1 hour: serve static (cached).
- After 1 hour: serve stale → trigger background regeneration → next request gets fresh.
On-demand revalidation (from a Server Action):
import { revalidatePath, revalidateTag } from "next/cache";
async function updatePost(slug: string) {
await db.posts.update({ where: { slug }, data: { ... } });
revalidatePath(`/blog/${slug}`); // invalidate this path
revalidateTag("posts"); // or invalidate all tagged "posts"
}
After mutation, immediately purge the cached page. Next request triggers regeneration.
Q: fetch cache directives.
A: Next.js wraps fetch with caching:
// Cache forever
const data = await fetch(url, { cache: "force-cache" });
// Never cache
const data = await fetch(url, { cache: "no-store" });
// Revalidate every 60 seconds
const data = await fetch(url, { next: { revalidate: 60 } });
// Tag for on-demand purging
const data = await fetch(url, { next: { tags: ["posts", "user-123"] } });
revalidateTag("posts") purges all fetch calls tagged "posts". Combine with revalidatePath for finer control.
Next 15 change: default cache: is now "no-store" (was "force-cache"). Less surprising; matches expectations. Older code may need cache: "force-cache" added explicitly.
Q: Edge runtime — when use?
A: Edge runtime uses V8 isolates (like Cloudflare Workers, Vercel Edge Functions) instead of Node.js:
export const runtime = "edge";
export default async function Page() {
// ...
}
Pros:
- Faster cold starts (~5ms vs 100-500ms for Node serverless).
- Closer to user (deployed to many edge locations).
- Lower TTFB for global users.
Cons:
- Limited APIs — no
fs, no Node native modules, restricted timers, smaller heap. - No long-running tasks (50ms CPU limit on Vercel by default).
- No
process.envfor build-time secrets (uses different env model). - Smaller ecosystem of compatible libraries.
Use for: read-heavy routes, simple SSR, geolocation-based personalization, header-based redirects. Don’t use for: routes needing DB drivers (most are Node-only), heavy compute, large dependencies.
Q: Partial Prerendering (PPR) — what is it?
A: Next 14+ feature (in beta): a single route can have a static shell + dynamic holes. The shell streams from cache immediately; the dynamic parts stream when they’re ready.
export const experimental_ppr = true;
export default function Dashboard() {
return (
<>
<Header /> {/* static */}
<Sidebar /> {/* static */}
<Suspense fallback={<Loading />}>
<UserStats /> {/* dynamic — uses cookies() */}
</Suspense>
</>
);
}
Behavior:
- The page shell is pre-rendered at build time (Header, Sidebar).
- The
<UserStats>boundary is rendered per-request. - Response: static shell streams from CDN instantly + dynamic chunk streams when ready.
Best of both worlds: ISR-grade serve speed for the static parts + fresh per-request data for the dynamic parts. Currently experimental; expect stabilization in Next 16+.
Q: Caching layers — what’s in Next.js?
A: Multiple:
- Request memoization — same
fetchcall within one render dedupes. - Data Cache (
fetchcache) — Next stores fetch results across requests (default:no-store; opt-in toforce-cacheorrevalidate). - Full Route Cache — entire rendered route cached per (route, search params).
- Router Cache — client-side cache of prefetched routes (for instant navigation).
- CDN cache — your hosting provider’s CDN (Vercel automatic).
Each layer can be controlled / purged independently. Most teams need to think about the Data Cache (fetch directives) and Router Cache (<Link prefetch>). The Full Route Cache is mostly automatic.
Q: Static + dynamic in the same route.
A: A layout can be static while a page is dynamic; a page can have static parts + dynamic Suspense boundaries (PPR or streaming SSR).
// app/(dashboard)/layout.tsx — static
export default function Layout({ children }) {
return <div className="layout"><Nav />{children}</div>;
}
// app/(dashboard)/page.tsx — dynamic
export const dynamic = "force-dynamic";
export default async function Page() {
const data = await fetchPersonalized();
return <Dashboard data={data} />;
}
The shell stays static; the page renders per-request. Useful for “marketing-style chrome + per-user content.”
Q: How do you debug “why is this route dynamic”?
A: Run next build and watch the output:
Route Size First Load JS
○ / 12 KB 100 KB
ƒ /dashboard 8 KB 95 KB
● /blog/[slug] 5 KB 90 KB
Symbols:
○static (pre-rendered at build).ƒdynamic (rendered per-request).●SSG (static at build withgenerateStaticParams).◐partially pre-rendered (PPR).
If a route you expected to be static shows ƒ, something triggered dynamic. Common culprits: cookies() in a deep import, searchParams use, fetch(..., { cache: "no-store" }). Trace from the build error or use next build --debug.
Q: When NOT to use Next.js’s caching?
A:
- Mutation-heavy APIs — every response is unique; caching wastes effort.
- Real-time feeds — sub-second freshness needed; ISR’s “60-second stale” isn’t acceptable.
- Per-user data where the user changes frequently — cache by user ID becomes the cache, not the URL.
Default to cache: "no-store" (Next 15 default) for these. Opt into caching deliberately, not by accident.
Gotchas / edge cases
- Mixing
force-staticwithcookies()— build error. The static route can’t use per-request APIs. searchParamsmakes the route dynamic — any page with?parameters can’t be statically pre-rendered (unless you list them ingenerateStaticParams).fetchcache key includes the URL + method + body — different bodies = different cache entries.- Cache stampede on revalidate — first request after revalidation triggers regen; concurrent requests during regen get stale (Next’s default) or all wait (depends on platform).
revalidatePathinvalidates the route cache, not the fetch cache — pair withrevalidateTagfor fetch invalidation.- Edge runtime + database driver — most drivers are Node-only. Use HTTP-based clients (Vercel Postgres, Drizzle’s HTTP adapter, Cloudflare D1).
- Build time + many pages —
generateStaticParamsreturning 100K paths = 100K builds. Use ISR +dynamicParams: trueto generate on-demand.
What a senior is expected to say
- “Per-route control via exported constants:
dynamic,revalidate,runtime. ISR viarevalidate = N; force static or dynamic withdynamic = ....” - “
fetchcache directives are explicit in Next 15+:cache: 'no-store'(default),cache: 'force-cache',next: { revalidate, tags }. On-demand purge withrevalidatePath/revalidateTag.” - “Edge runtime for fast cold-start + low TTFB; trade smaller heap + limited APIs. Pair with HTTP-based clients (Vercel Postgres, etc.).”
- “Partial Prerendering (experimental Next 14+) gives static shell + dynamic holes — best of both worlds. Watch for stabilization.”
- “Build output symbols (○ static, ƒ dynamic, ● SSG, ◐ PPR) tell you what each route compiled to — debug ‘why is this dynamic’ from there.”
- “Caching is layered: request memo + Data Cache (fetch) + Full Route Cache + Router Cache + CDN. Each is controllable; most teams tune Data Cache and Router Cache deliberately.”
Cross-references
- App Router layout / segments: ../12_project_structure/04_nextjs_app_router_layout.md
- RSC deeper: 03_rsc_deeper.md
- Streaming SSR: 04_streaming_ssr_with_suspense.md
- Caching headers (HTTP layer): ../18_browser_internals/08_caching_headers.md
Further reading
- Next.js — Caching: https://nextjs.org/docs/app/building-your-application/caching
- Next.js — Route Segment Config: https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
- Next.js — Edge Runtime: https://nextjs.org/docs/app/api-reference/edge
- Partial Prerendering: https://nextjs.org/docs/app/api-reference/next-config-js/ppr