How it works
The explanation, with examples you can run.
What the build tells you
A throwaway app built with next build on Next.js 16.4.2 printed:
Route (app) Revalidate Expire├ ○ /plain├ ○ /api/static├ ƒ /api/time├ /blog/[slug]│ ├ ◐ /blog/[slug]│ ├ ○ /blog/a│ └ ○ /blog/b├ ○ /cached 1h 1d├ ƒ /cookie-nosuspense├ ◐ /cookie-suspense└ ◐ /random ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed contentƒ (Dynamic) server-rendered on demand/plainrenders only markup: fully static./cookie-suspensereadscookies()inside a Suspense boundary: a shell plus one streamed hole./randomcallsconnection()beforecrypto.randomUUID(), which says "this must be per request"; also a hole./cachedawaits a function markeduse cachewithcacheLife("hours"): its result is in the shell, with a 1 hour revalidate and 1 day expiry./blog/aand/blog/bcame fromgenerateStaticParams; any other slug gets the reusable shell (/blog/[slug]) with its param-specific parts behind fallbacks./cookie-nosuspenseread cookies with no boundary. The first build failed with "Next.js encountered uncached or runtime data during prerendering" and listed three fixes. It only built once the route explicitly opted out of the check, and then it is ƒ: nothing is prerendered and every visit waits for the server.
Thinking in shells and holes
export default function Page() { return ( <> <Header /> {/* in the shell */} <ProductList /> {/* "use cache": in the shell */} <Suspense fallback={<CartSkeleton />}> {/* fallback in the shell */} <Cart /> {/* reads cookies(): streamed */} </Suspense> </> )}The deeper request data sits in the tree, the more of the page is static. A layout that awaits params at the top makes everything below it wait; passing the params promise down and awaiting it in a small child keeps the layout in the shell.
Suspense doesn't make anything dynamic by itself; it only gives a place to show a fallback. Values that differ per run, such as Math.random(), Date.now() and crypto.randomUUID(), must be either cached (everyone sees the same value until revalidation) or placed after await connection() inside a boundary.
Apps without Cache Components use the previous model: a route is static unless it touches request data anywhere, in which case the whole route becomes dynamic, and options like export const dynamic = "force-dynamic" override that per segment. Those segment options are removed when Cache Components is on.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.