frontend / rendering modes / 05_nextjs_rendering_control.md

Next.js App Router Rendering Choices — Per-Route Control

7 min read source

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).
  • searchParams prop (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.env for 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:

  1. Request memoization — same fetch call within one render dedupes.
  2. Data Cache (fetch cache) — Next stores fetch results across requests (default: no-store; opt-in to force-cache or revalidate).
  3. Full Route Cache — entire rendered route cached per (route, search params).
  4. Router Cache — client-side cache of prefetched routes (for instant navigation).
  5. 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 with generateStaticParams).
  • 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-static with cookies() — build error. The static route can’t use per-request APIs.
  • searchParams makes the route dynamic — any page with ? parameters can’t be statically pre-rendered (unless you list them in generateStaticParams).
  • fetch cache 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).
  • revalidatePath invalidates the route cache, not the fetch cache — pair with revalidateTag for 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 pagesgenerateStaticParams returning 100K paths = 100K builds. Use ISR + dynamicParams: true to generate on-demand.

What a senior is expected to say

  • “Per-route control via exported constants: dynamic, revalidate, runtime. ISR via revalidate = N; force static or dynamic with dynamic = ....”
  • fetch cache directives are explicit in Next 15+: cache: 'no-store' (default), cache: 'force-cache', next: { revalidate, tags }. On-demand purge with revalidatePath / 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

Further reading