Partial Prerendering

Partial Prerendering and the Static Shell

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.

What a build keeps and what it defers
What a build keeps and what it defers

next build prints the verdict per route, with the effective lifetime of each one:

Output of 40
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.