Two questions decide it. Does the HTML differ per request? How fresh
must it be? Same for everyone, can be stale → static. Same for
everyone, must be current → static + revalidate (ISR). Differs
per user/request → dynamic. Needs the browser → a 'use client'
island inside whichever of those.
You're here when
- A new route, and you're choosing between "just render it",
generateStaticParams,revalidate, or'use client' - A page that should be instant is doing a server round-trip on every request
next buildmarks a routeƒ(Dynamic) when you expected○(Static)- You added
cookies(),headers(), orsearchParamsand something got slow
The play
- 1Start from the default: a route is prerendered at build time unless something opts it out. You don't add config to be static: you already are.
- 2Does the HTML differ per request? If no, keep it static. If it's static with per-URL params (a slug), list them from
generateStaticParams; the build marks it●(SSG). - 3Must it refresh without a redeploy? Add
export const revalidate = <seconds>(orfetch(url, { next: { revalidate } })per call). Still served static, rebuilt in the background: that's ISR. - 4Does it genuinely differ per user or request (auth, geo, a
searchParamsthat changes the body)? Then let it be dynamic: readingcookies()/headers()/ dynamicsearchParamsopts it in, or setexport const dynamic = 'force-dynamic'to be explicit. - 5Anything that needs the browser (
onClick,useState,window, a socket) goes in a'use client'component. Keep the route's shell on whichever server strategy above; don't make the page a client component to get one button. - 6Check
next build:○static,●SSG,ƒdynamic. If a route flipped toƒyou didn't expect, find the request-time API that did it.
Which path
Same for every visitor
- Static. Add
revalidateonly if it must refresh without a redeploy. - Never reach for
force-dynamichere: you'd trade an instant CDN hit for a server render to get nothing.
Differs per visitor or request
- Dynamic, but keep the dynamic part small.
- A static shell with one dynamic slot (or a client component fetching after load) beats making the whole route SSR.
Gotchas
- 1
force-dynamic to 'fix' stale data
That makes every hit a server render. If you only need freshness,
revalidatekeeps the route static and fast. - 2
One cookies() call in a shared layout
It opts every route under that layout into dynamic rendering. Read request APIs as deep in the tree as possible, or in a client component.
- 3
'use client' at the top of the page file
It doesn't make "a client page": it makes that file and everything it imports client-side, so the bundle grows. Push the boundary down to the smallest interactive piece.
- 4
Assuming searchParams is free
Reading
searchParamsin a Server Component marks the route dynamic. If the value set is small and known, put it in the path and usegenerateStaticParamsinstead. - 5
dynamic and revalidate fighting
force-dynamicwins and silently disablesrevalidate. Pick one model per route.
Confirm you're clear
- 1
next buildshows the symbol you intended (○/●/ƒ) for the route. - 2Nothing request-specific is read above the point where dynamic rendering is actually needed.
- 3Client bundles carry only the components that truly need the browser.