Static vs Dynamic Rendering
Understand when Next.js renders routes at build time (static) versus request time (dynamic), and how to control it.
Search across all documentation pages
Understand when Next.js renders routes at build time (static) versus request time (dynamic), and how to control it.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card -- copy-paste ready.
// Static by default -- no dynamic functions, no uncached fetches
export default async function AboutPage() {
return <h1>About Us</h1>; // Built at deploy time
}
// Becomes dynamic automatically when using dynamic functions
import { cookies } from "next/headers";
export default async function DashboardPage() {
const cookieStore = await cookies(); // This opts into dynamic rendering
return <h1>Dashboard</h1>;
}
// Force static or dynamic with segment config
export const dynamic = "force-dynamic"; // Always render at request time
export const dynamic = "force-static"; // Always render at build time
// Pre-render dynamic routes at build time
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}When to reach for this: You need to understand why a route is static or dynamic, or you want to explicitly control the rendering behavior.
// app/blog/[slug]/page.tsx -- Static generation with generateStaticParams
import { notFound } from "next/navigation";
type Post = { slug: string; title: string; content: string; date: string };
async function getPost(slug: string): Promise<Post | null> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: [`post-${slug}`] },
});
if (!res.ok) return null;
return res.json();
}
// Pre-render these paths at build time
export async function generateStaticParams() {
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Generate metadata for each post
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Not Found" };
return { title: post.title };
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article className="prose max-w-2xl mx-auto p-6">
<h1>{post.title}</h1>
<time className="text-gray-500">{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}// app/dashboard/page.tsx -- Dynamic rendering (uses cookies)
import { cookies } from "next/headers";
import { db } from "@/lib/db";
import { verifySession } from "@/lib/auth";
export default async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get("session")?.value;
const session = await verifySession(token);
if (!session) {
return <p>Please sign in</p>;
}
const stats = await db.userStats.findUnique({
where: { userId: session.userId },
});
return (
<main className="p-6">
<h1 className="text-2xl font-bold">Welcome, {session.user.name}</h1>
<div className="grid grid-cols-3 gap-4 mt-6">
<StatCard label="Orders" value={stats?.orders ?? 0} />
<StatCard label="Revenue" value={`$${stats?.revenue ?? 0}`} />
<StatCard label="Customers" value={stats?.customers ?? 0} />
</div>
</main>
);
}
function StatCard({ label, value }: { label: string; value: string | number }) {
return (
<div className="border rounded p-4">
<p className="text-sm text-gray-500">{label}</p>
<p className="text-2xl font-bold">{value}</p>
</div>
);
}What this demonstrates:
generateStaticParamscookies()cookies(), headers(), searchParams)cache: "no-store" on any fetchdynamic = "force-dynamic"cookies(), headers(), searchParams (the page prop), useSearchParams() (without Suspense), and connection().generateStaticParams tells Next.js which dynamic route segments to pre-render at build time. Paths not returned by this function are either rendered on-demand (and cached) or return 404, depending on dynamicParams.revalidate to rebuild static pages in the background after a time interval. The first request after the interval gets the stale page; subsequent requests get the fresh version.Controlling behavior for paths not in generateStaticParams:
// Allow rendering of paths not pre-generated (default behavior)
export const dynamicParams = true;
// Return 404 for paths not pre-generated
export const dynamicParams = false;Segment-level revalidation:
// Revalidate all pages under this layout every 60 seconds
export const revalidate = 60;
// Never revalidate (fully static until next deploy)
export const revalidate = false;Mixing static and dynamic on the same page with Suspense:
import { Suspense } from "react";
import { cookies } from "next/headers";
// The static shell renders at build time
export default function Page() {
return (
<main>
<h1>Product Page</h1> {/* Static */}
<StaticProductInfo /> {/* Static */}
<Suspense fallback={<p>Loading cart...</p>}>
<DynamicCartPreview /> {/* Dynamic -- calls cookies() */}
</Suspense>
</main>
);
}
async function DynamicCartPreview() {
const cookieStore = await cookies();
const cartId = cookieStore.get("cart-id")?.value;
// ...
}// generateStaticParams return type
export async function generateStaticParams(): Promise<{ slug: string }[]> {
// ...
}
// For nested dynamic routes
export async function generateStaticParams(): Promise<{
category: string;
slug: string;
}[]> {
// ...
}
// Segment config types
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const dynamicParams: boolean = true;
export const revalidate: number | false = false;
export const runtime: "nodejs" | "edge" = "nodejs";A single cookies() call makes the entire route dynamic -- Even if 99% of the page is static content, one dynamic function opts the whole route into dynamic rendering. Fix: Use Partial Prerendering (PPR) or isolate the dynamic part in a Suspense boundary.
generateStaticParams runs at build time only -- New content added after the build is not pre-rendered until the next build or unless dynamicParams is true (which renders on-demand). Fix: Keep dynamicParams: true (the default) so new paths are rendered on first request and then cached.
force-static with dynamic functions throws an error -- If you export dynamic = "force-static" but the page calls cookies(), the build fails. Fix: Remove the dynamic function or change to dynamic = "auto".
revalidate: 0 does not mean "revalidate immediately" -- It means "always dynamic." Fix: Use a positive number for ISR behavior.
Static pages with client-side data fetching -- A page can be statically generated but still fetch data on the client with SWR or React Query. The static HTML serves as the shell. Fix: This is a valid pattern, not a bug -- just be aware of the initial empty state.
| Approach | Use When | Don't Use When |
|---|---|---|
| Static Generation (default) | Content changes infrequently, can be built at deploy time | Data is user-specific or real-time |
ISR (revalidate: N) | Content changes periodically, you want CDN speed with freshness | Data must be fresh on every single request |
| Dynamic Rendering | Data is user-specific, uses cookies or headers | Content is the same for all users |
| Partial Prerendering (PPR) | You want a static shell with dynamic holes | The entire page is either fully static or fully dynamic |
| Client-side fetching | You need real-time updates after the page loads | Data can be fetched entirely on the server |
force-static | You want to guarantee a route is never dynamic | The route genuinely needs request-time data |
A route is static by default. It becomes dynamic if it:
cookies(), headers(), searchParams, connection())cache: "no-store" on a fetchdynamic = "force-dynamic"generateStaticParams tells Next.js which dynamic route segments to pre-render at build time.dynamicParams is true) or return 404.revalidate: N): pages are rebuilt in the background after N seconds. The first request after the interval gets the stale page; subsequent requests get the fresh version.<Suspense> boundary so only that portion is dynamic.The build will fail with an error. You cannot force a route to be static when it uses dynamic functions. Remove the dynamic function or change to dynamic = "auto".
It means always render dynamically (equivalent to force-dynamic). It does not mean "revalidate immediately." Use a positive number (e.g., revalidate: 60) for ISR behavior.
Yes. A page can be statically generated but still fetch data on the client using SWR or React Query. The static HTML serves as the shell, and client-side data fills in after hydration.
// Allow on-demand rendering for unknown paths (default)
export const dynamicParams = true;
// Return 404 for unknown paths
export const dynamicParams = false;export async function generateStaticParams(): Promise<
{ slug: string }[]
> {
const posts = await fetchAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}For nested dynamic routes, include all segment params in the return type.
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const dynamicParams: boolean = true;
export const revalidate: number | false = false;
export const runtime: "nodejs" | "edge" = "nodejs";Wrap the dynamic part in a <Suspense> boundary. The static shell renders at build time, and the dynamic component (e.g., one that calls cookies()) streams in at request time.
Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥