Static and Dynamic Rendering
The idea in one sentence
Section titled “The idea in one sentence”Next.js prerenders every route as static HTML at build time unless the route reaches for request-time data — at which point it becomes dynamic and renders on each request.
Static is the default
Section titled “Static is the default”A route with no request-time inputs is rendered once at build time and served as static HTML. Nothing runs per request; the same bytes go to everyone.
// app/about/page.tsx — no dynamic APIs, so this is prerendered at build timeexport default function AboutPage() { return <h1>About us</h1>;}This is the fastest path: the HTML sits on a CDN and returns instantly. Most marketing, docs, and content pages should stay here.
What flips a route to dynamic
Section titled “What flips a route to dynamic”A route switches to dynamic rendering the moment it depends on something only known at request time. Any of these will flip it:
- A Dynamic API —
cookies(),headers(),draftMode(), or readingsearchParams/ theparamsPromise at request time. - An uncached data request — a
fetch()withcache: 'no-store'(or any fetch under Next.js 15+ defaults that you have not opted into caching). - An explicit
export const dynamic = 'force-dynamic'.
// app/dashboard/page.tsx — reading cookies() flips this route to dynamicimport { cookies } from 'next/headers';
export default async function Dashboard() { const store = await cookies(); const theme = store.get('theme')?.value ?? 'light'; return <p>Active theme: {theme}</p>;}You can also force the mode yourself with a segment config export:
// Force a route to render on every request, even with no dynamic APIexport const dynamic = 'force-dynamic';generateStaticParams prerenders dynamic segments
Section titled “generateStaticParams prerenders dynamic segments”A dynamic segment like [slug] has no fixed value at build time. generateStaticParams tells Next.js exactly which values to prerender, turning a dynamic route into a set of static pages — the App Router replacement for the old getStaticPaths.
export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then((r) => r.json()); return posts.map((post) => ({ slug: post.slug }));}
export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return <article>Reading post: {slug}</article>;}Each returned slug is built into its own static HTML file at build time.
Reading the build output
Section titled “Reading the build output”next build prints a legend that tells you the mode of every route. Learn the three symbols and you never have to guess:
Route (app) Rendering○ /about Static prerendered as static HTMLƒ /dashboard Dynamic server-rendered on each request● /blog/[slug] SSG prerendered via generateStaticParamsIf a route you expected to be static shows ƒ, something inside it reached for request-time data — trace back to the Dynamic API or uncached fetch that caused it.
flowchart TD
A[Incoming request] --> B{Dynamic API or uncached fetch?}
B -- No --> C[Static render, prerendered at build]
B -- Yes --> D[Dynamic render, on each request]
E[generateStaticParams] --> C