Dynamic vs Static: Controlling the Boundary
Know exactly what tips a route into dynamic rendering, and use the route segment config to steer it deliberately.
Every route resolves to static (rendered once, cached, reused) or dynamic (rendered per request). Next infers this — but a senior engineer decides it on purpose and knows which lines of code flip the switch.
What forces dynamic
- Reading request-bound APIs:
cookies(),headers(),draftMode(), or thesearchParamsprop. - A fetch with
cache: 'no-store'. - Exporting
export const dynamic = 'force-dynamic'. - Using the uncached function
connection()(Next 15) to explicitly opt out.
If none of these appear, the route is static and blazing fast.
The route segment config knobs
| Export | Effect |
|---|---|
dynamic = 'force-static' | force static; dynamic APIs return empty/default |
dynamic = 'force-dynamic' | force per-request rendering |
revalidate = 60 | ISR window for the whole route |
fetchCache | override default fetch caching |
dynamicParams = false | 404 params not returned by generateStaticParams |
The key insight
Dynamic is not the enemy — accidental dynamic is. A stray cookies() read high in a shared layout can silently turn an entire subtree dynamic and quietly kill your static performance. Keep per-request reads low and narrow, and let Partial Prerendering (next lesson) give you the best of both.
Example
// Deliberately static, revalidated hourly — even though the API
// is dynamic-capable, we pin the behavior explicitly.
export const dynamic = 'force-static';
export const revalidate = 3600;
export default async function MarketingStats() {
const res = await fetch('https://api.example.com/stats');
const data = await res.json();
return <p>Trusted by {data.customers} teams</p>;
}
// Contrast: this route is intentionally dynamic because
// it personalizes on a per-request cookie.
// export const dynamic = 'force-dynamic';
// import { cookies } from 'next/headers';
// const theme = (await cookies()).get('theme')?.value;When to use it
- A developer audits which routes are dynamic by running next build and examining the route type column in the output.
- A team wraps the personalised header in a Client Component behind Suspense so the static page body still pre-renders.
- An engineer uses unstable_noStore() inside a utility function to force a specific data call to bypass the cache without changing the route.
More examples
What makes a route dynamic
Calling cookies(), headers(), or using cache:'no-store' signals to Next.js that the route cannot be pre-rendered.
import { cookies, headers } from 'next/headers';
// Any of these in a page cause dynamic rendering:
const cookieStore = await cookies(); // reads request cookies
const reqHeaders = await headers(); // reads request headers
const res = await fetch(url, { cache: 'no-store' }); // uncached fetchKeep static body, dynamic header
Suspense isolates the dynamic UserGreeting inside a boundary, letting the static article body pre-render as HTML.
import { Suspense } from 'react';
import UserGreeting from './UserGreeting'; // reads cookies
export default function Page() {
return (
<>
<Suspense fallback={<div className="h-10" />}>
<UserGreeting />
</Suspense>
<article>Static article content rendered at build time</article>
</>
);
}unstable_noStore for utility functions
unstable_noStore() inside a helper marks only that call as uncached, rather than forcing the whole route to be dynamic.
import { unstable_noStore as noStore } from 'next/cache';
export async function getRandomQuote() {
noStore(); // opt this call out of caching without changing the route
const res = await fetch('https://api.quotable.io/random');
return res.json();
}
Discussion