Static vs Dynamic Rendering
Understand when Next.js renders routes at build time (static) versus request time (dynamic), and how to control it.
Recipe
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.
Working Example
// 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:
- A blog post page that is statically generated at build time using
generateStaticParams - A dashboard page that is dynamically rendered because it calls
cookies() - Metadata generation for static pages
- The fetch result is cached and revalidated by tag
Deep Dive
How It Works
- By default, Next.js tries to statically render every route at build time. A route remains static as long as it:
- Does not call dynamic functions (
cookies(),headers(),searchParams) - Does not use
cache: "no-store"on any fetch - Does not export
dynamic = "force-dynamic"
- Does not call dynamic functions (
- Dynamic functions that opt a route into dynamic rendering:
cookies(),headers(),searchParams(the page prop),useSearchParams()(without Suspense), andconnection(). generateStaticParamstells 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 ondynamicParams.- Incremental Static Regeneration (ISR) uses
revalidateto 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. - Static pages are served from the CDN edge with zero server computation. Dynamic pages require a server (Node.js or Edge runtime) for each request.
Variations
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;
// ...
}TypeScript Notes
// 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";Gotchas
-
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. -
generateStaticParamsruns at build time only -- New content added after the build is not pre-rendered until the next build or unlessdynamicParamsistrue(which renders on-demand). Fix: KeepdynamicParams: true(the default) so new paths are rendered on first request and then cached. -
force-staticwith dynamic functions throws an error -- If you exportdynamic = "force-static"but the page callscookies(), the build fails. Fix: Remove the dynamic function or change todynamic = "auto". -
revalidate: 0does 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.
Alternatives
| 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 |
FAQs
How does Next.js decide if a route is static or dynamic?
A route is static by default. It becomes dynamic if it:
- Calls a dynamic function (
cookies(),headers(),searchParams,connection()) - Uses
cache: "no-store"on a fetch - Exports
dynamic = "force-dynamic"
What is generateStaticParams and when does it run?
generateStaticParamstells Next.js which dynamic route segments to pre-render at build time.- It runs at build time only, not at request time.
- Paths not returned are rendered on-demand (if
dynamicParamsistrue) or return 404.
What is the difference between static generation and ISR?
- Static generation: pages are built once at deploy time and never change until the next build.
- ISR (
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.
Gotcha: A single cookies() call makes my entire page dynamic. How do I fix this?
- Use Partial Prerendering (PPR) to keep the static shell and stream the dynamic part.
- Alternatively, isolate the dynamic component inside a
<Suspense>boundary so only that portion is dynamic.
Gotcha: What happens if I export force-static but the page calls cookies()?
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".
What does revalidate: 0 mean?
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.
Can a statically generated page still fetch data on the client?
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.
How do I control what happens for paths not returned by generateStaticParams?
// Allow on-demand rendering for unknown paths (default)
export const dynamicParams = true;
// Return 404 for unknown paths
export const dynamicParams = false;How do I type the return value of generateStaticParams in TypeScript?
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.
What are the valid TypeScript types for the dynamic segment config export?
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";How can I mix static and dynamic content on the same page?
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.
Where are static pages served from versus dynamic pages?
- Static pages are served from the CDN edge with zero server computation.
- Dynamic pages require a server (Node.js or Edge runtime) for each request.
Related
- Server Components -- Async components that enable static rendering
- Partial Prerendering -- Hybrid static + dynamic
- Revalidation -- ISR and on-demand revalidation
- Caching -- How caching layers determine rendering mode