PPR compilation model, how Next.js analyses the Suspense tree, static shell generation at build time, dynamic slot streaming at request time, dynamicIO and use cache as modern PPR primitives, and the PPR vs ISR vs SSR decision matrix.
A-4 — Partial Prerendering: Static Shell, Dynamic Holes
Who this is for: Architects who need to understand PPR at the implementation level — not just "static parts serve from CDN, dynamic parts stream from server" but how Next.js analyses the component tree at build time, what the PPR payload looks like, and when PPR is the right architectural choice versus ISR or SSR.
The Problem PPR Solves
Before PPR existed, every page in a Next.js application had to choose a single rendering strategy. A product page with mostly static content (images, description, specs) but one dynamic element (personalised price, inventory count) had three bad options:
- SSG: Fast but stale. The inventory count is wrong the moment the build finishes.
- SSR: Always fresh but every request hits the server. A product page with 100k monthly views at 200ms server render time = 5.5 server-hours per day just to render that one page.
- ISR: Better, but the stale window means inventory can be wrong for up to N seconds. And if the product goes out of stock, users might still see "In Stock" for up to the revalidation period.
PPR's answer: split the page. The static shell — everything that doesn't change per-user or per-request — is pre-rendered at build time and served from the CDN instantly. The dynamic holes — inventory count, personalised price, "you've viewed this before" message — are streamed from the server immediately after, in parallel with the CDN response.
The user receives the static shell in ~5ms (from CDN). The dynamic slots arrive ~50-150ms later (from the server). The perceived TTFB is the CDN response time; the personalised data arrives almost as fast as full SSR but without the CDN bypass.
How PPR Works at Build Time
PPR is enabled per-page with an experimental flag:
Per-page opt-in (with ppr: 'incremental'):
At build time, Next.js renders the page statically — following the same path as generateStaticParams. It encounters Suspense boundaries during this render. For each Suspense boundary, it asks: "does the content inside this boundary use any dynamic APIs (cookies, headers, connection, dynamic fetch)?"
- Content inside Suspense with only static data → baked into the static shell
- Content inside Suspense that uses dynamic APIs → becomes a dynamic hole placeholder
The static shell is stored in the Full Route Cache. The dynamic hole positions are marked with placeholder nodes.
What a PPR Response Looks Like
When a user requests a PPR page:
The CDN response for the static shell includes a preconnect hint to the origin server so the browser opens a connection to the server while rendering the shell — minimising the gap between shell arrival and dynamic content arrival.
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion