Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 12 min read

Static or dynamic

When a page is produced at build time – and when on every request

In one sentence

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

options.txt
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
Put another way

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

derivation.txt
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.
Tip The build output tells you everything

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.

app/blog/[slug]/page.tsx
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>;
}
One finished file per post at build time – as fast as a home page.
Good to know What happens with unknown values

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

Something personal on an otherwise static page
the whole page turns dynamic
export default async function Home() {
  const user = await readFromCookie();   // makes EVERYTHING dynamic
  const posts = await loadPosts();

  return (
    <>
      <Greeting user={user} />
      <PostList posts={posts} />
    </>
  );
}
only the personal part
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

steering.tsx
// 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
Careful force-dynamic is rarely the right answer

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
build-output.txt
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.

Tip Make it part of your review

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.

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →