Static or dynamic
When a page is produced at build time – and when on every request
Next.js decides for itself whether a page is static or dynamic – based on what you use. Knowing how it decides means steering it deliberately instead of by accident.
There is no “please make this page static” switch. Next.js looks at what your page needs and derives when it can be produced. This lesson makes that derivation visible.
The three options
Static produced once at build time, identical for all
→ as fast as it gets, costs almost nothing
Home page, blog posts, documentation
Static + time the same, but with an expiry date (revalidate)
→ current enough without work per request
Price list, news overview
Dynamic produced fresh on every request
→ necessary as soon as it gets personal
Account, cart, dashboard Static is a printed poster: produced once, hung up a thousand times. Dynamic is a handwritten note per person. And revalidate is the poster reprinted every morning.
What Next.js goes by
The page stays static as long as none of these appear:
cookies() → needs the specific request
headers() → same
searchParams → same
no-store on a fetch
export const dynamic = "force-dynamic"
Without all of that, Next.js can finish the page at build time. npm run build lists every address with a symbol: ○ static, ƒ dynamic, ● produced at build time from generateStaticParams. If you see something other than expected, you have found the cause before reading a line of code.
Making dynamic addresses static
A page with [slug] is dynamic at first – Next.js does not know the possible values. With generateStaticParamsTells Next.js at build time which concrete values a dynamic segment should take – so it can turn them into static pages.→ you name them, and one address becomes many finished pages.
export async function generateStaticParams() {
const posts = await loadAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await loadPost(slug);
return <article>{post.body}</article>;
} If somebody requests a slug that was not in the list, Next.js builds that page on first access and remembers it. If you do not want that, set export const dynamicParams = false – then everything else is a 404.
The most common mistake
export default async function Home() {
const user = await readFromCookie(); // makes EVERYTHING dynamic
const posts = await loadPosts();
return (
<>
<Greeting user={user} />
<PostList posts={posts} />
</>
);
} export default async function Home() {
const posts = await loadPosts(); // stays static
return (
<>
<Suspense fallback={<GreetingSkeleton />}>
<Greeting /> {/* reads the cookie itself */}
</Suspense>
<PostList posts={posts} />
</>
);
} The dynamic dependency now lives in its own component behind <Suspense>. The rest of the page can still be produced at build time.
Steering it deliberately
// in page.tsx or layout.tsx – applies to everything below
export const dynamic = "force-dynamic"; // always fresh
export const dynamic = "force-static"; // always static (errors on cookies())
export const revalidate = 3600; // static with an expiry date It is tempting when something will not go current. But it gives up the framework's biggest advantage. Check first whether revalidate or a targeted revalidateTag is enough.
▸ In more depth: what really happens at build time optional
Route (app) Size First Load JS
┌ ○ / 1.2 kB 92 kB
├ ● /blog/[slug] 850 B 91 kB
│ ├ /blog/hello-world
│ └ /blog/next-15
├ ƒ /account 2.1 kB 95 kB
└ ○ /imprint 180 B 88 kB
○ static produced at build time
● SSG produced from generateStaticParams
ƒ dynamic on every request Two columns are worth a look. The symbol shows whether your intention worked out. And “First Load JS” is the JavaScript a user has to download before the page responds – if that number suddenly rises, a client boundary has usually slipped upwards.
It pays to glance at the build output on bigger changes. A page that quietly moved from ○ to ƒ costs compute on every request from the next deploy onwards – and otherwise you only notice on the bill.
Does it stick?
5 questions on this lesson. Wrong answers show up in your stats.