Skip to content

Static and Dynamic Rendering

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.

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 time
export 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.

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 APIcookies(), headers(), draftMode(), or reading searchParams / the params Promise at request time.
  • An uncached data request — a fetch() with cache: '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 dynamic
import { 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 API
export 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.

app/blog/[slug]/page.tsx
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.

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 generateStaticParams

If 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
How Next.js decides static vs dynamic
How does Next.js render a route by default?
Which of these flips a route to dynamic rendering?
What does generateStaticParams do?
In the build output, what does the ƒ symbol mean?