
Partial Prerendering became stable in Next.js 16 as part of Cache Components, enabled with a single flag: cacheComponents: true. That one-line change resolves a trade-off Next.js developers have been manually working around since the framework existed: a page is either fast because it’s static, or personalized because it’s dynamic — never both, at least not without extra plumbing.
The Problem PPR Actually Solves
Take a typical e-commerce product page. The product name, description, images, and reviews are the same for every visitor and rarely change — perfect for static generation. But the same page also shows a personalized “recommended for you” rail and a live stock count. Under the old all-or-nothing model, that one dynamic widget forces the entire page into server-side rendering, and the fast static content pays the same latency tax as the slow personalized content.
Partial Prerendering splits a single route into two parts that ship in one HTTP response: a static shell generated at build time, and dynamic “holes” that stream in at request time.
How It Actually Works
- At build time, Next.js prerenders everything it can into a static HTML shell, with React Suspense fallbacks marking the spots where dynamic content will appear. It also produces a “postponed state” — an opaque value the server uses to resume rendering later.
- At request time, the static shell is served immediately, often straight from the edge cache, so Time to First Byte is close to edge latency regardless of what the dynamic parts are doing.
- In parallel, the server resumes rendering from the postponed state, executing only the components inside the Suspense boundaries, and streams each one into the same response as it resolves — using standard HTTP chunked transfer encoding, not a second round-trip fetch.
From the browser’s perspective, there’s no second request. The dynamic content simply arrives as additional chunks of the same response the static shell came in on.
Where to Put the Suspense Boundaries
Boundary placement is the whole game. Get it wrong and you lose the benefit without realizing it.
- If your
<h1>or primary content sits inside a Suspense boundary, it has moved out of the prerendered shell. It still arrives in the same response, but the TTFB win for that content is gone. - A wide Suspense boundary placed high in the component tree — wrapping an entire layout, for instance — pulls the whole subtree into the dynamic bucket. Keep boundaries narrow and specific to the truly personalized elements.
- Good candidates for dynamic holes: shopping carts, personalized recommendations, live notification counts, “logged in as” widgets. Keep the primary content, layout, and navigation in the static shell.
generateMetadatacan become a request-time bottleneck if it reads cookies or headers. Next.js 15.2+ supports streaming metadata so a slow metadata function doesn’t block the shell, but static or cached metadata — title, description, canonical, OG tags — is still the safer default.
What Happens on a Cold Cache
When the shell is cached at the edge, the CDN serves it immediately. On a cold cache, or in a region that doesn’t have the shell cached yet, the response falls back to a regular SSR round-trip instead — PPR degrades gracefully rather than breaking.
Best Use Cases
- E-commerce pages with static product details and dynamic pricing, stock, or recommendations.
- Dashboards with a stable layout and isolated widgets that need live data.
- Any route with a large stable surface area and one or two genuinely personalized sections — the worse the old all-or-nothing trade-off was for a given page, the bigger the PPR win.
The Bottom Line
Partial Prerendering isn’t a tweak to SSG or SSR — it’s a third model that combines React Suspense, streaming, and edge caching to deliver both at once, on the same route. For most Next.js applications built on the App Router today, it should be the default rendering strategy rather than a special-case optimization saved for the busiest pages.