Partial Prerendering is no longer a flag. With Cache Components on, every build renders the whole component tree and keeps whatever it can finish: static JSX, cached results, and the <Suspense> fallbacks standing in for everything else. That output is the static shell — HTML for first loads, an RSC payload for client navigations — and it can be served straight from a CDN.

next build prints the verdict per route, with the effective lifetime of each one:
Route (app) Revalidate Expire ┌ ○ / ├ ○ /_not-found ├ ○ /cached 30d 1y ├ ◐ /greeting ├ ◐ /live ├ ○ /notes 30d 1y ├ /products/[id] │ ├ ◐ /products/[id] │ ├ ○ /products/1 1d 1w │ └ ○ /products/2 1d 1w └ ○ /timed 5s 1h ○ (Static) prerendered as static content ◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
There is no fully dynamic ƒ row: even /live, whose whole body waits on connection(), ships a shell with its heading and fallback, and the timestamp follows on the same response.
Push the boundary down
How much lands in the shell depends on where you await. A layout that destructures params at the top ties its whole output to one URL; passing the promise into a child inside <Suspense> keeps the sidebar and the children in the shell and streams only the heading. The same holds for cookies(), headers() and any uncached fetch. Keep async work as deep in the tree as it will go.