Partial Prerendering is stable in Next.js 16: how to structure routes
Next.js 16 makes Partial Prerendering stable via cacheComponents: a static shell from the CDN edge, dynamic parts streamed in the same response.
Partial Prerendering is a rendering model in Next.js that serves one route as a static shell and streams its dynamic parts into the same HTTP response. The shell (layout, navigation, headers, anything identical for every visitor) ships from the CDN edge in milliseconds. The personalized parts (a dashboard row, a cart total, a feature flag) render at request time and fill the placeholders React already drew.
It went stable in Next.js 16, released in October 2025. The old experimental.ppr flag is gone. You now turn it on through a broader model called Cache Components. If you build SaaS on the App Router, this changes how you decide what is static and what is dynamic, route by route. That decision used to be all-or-nothing per page. Now it is per component.
The 30-second version
Before PPR, a Next.js route was either static or dynamic. One cookies() call, one uncached fetch, and the whole page became dynamic: every visitor waited on the server, the CDN cached nothing, and time to first byte tracked your slowest query. PPR breaks that trade-off. The static shell is prerendered at build time and served from the edge. Dynamic work happens only inside Suspense boundaries, and only those parts wait. The reader sees layout instantly and watches the dynamic holes fill in.
Why did this take three versions to ship?
PPR arrived as an experimental flag in Next.js 14, got refined in 15, and shipped stable in 16. The delay was not marketing. The hard part is correctness: Next.js has to prove, at build time, which parts of a component tree are truly static and which depend on request data, then serialize the static output plus a "postponed" blob that lets the server resume rendering the dynamic parts per request. Get that boundary wrong and you either leak one user's data into another's cache or you make everything dynamic and lose the point.
Next.js 16 fixed the ergonomics by flipping the default. With Cache Components, everything is dynamic unless you opt in. You mark what should be cached with the 'use cache' directive, and you mark what must stay dynamic by wrapping it in Suspense. The framework stops guessing.
How does Partial Prerendering actually work?
Three phases, and it helps to keep them separate in your head.
Build time. Next.js renders the parts of the route that do not depend on request data: the layout, static copy, and the fallback UI of every Suspense boundary. It stores that as a static HTML shell plus a serialized blob of the paused render.
Request time. The shell ships first, straight from the CDN edge. Because it is the same bytes for everyone, edge latency (roughly 20 to 50ms) is the whole cost. The server then resumes the paused render, produces the dynamic sections, and streams them down the same connection using chunked transfer encoding. No second request.
Hydration. React hydrates each Suspense boundary in place as its chunk arrives. The user reads the shell while the holes are still loading, which is the entire performance argument.
The mechanism is worth reading in full from a Next.js core engineer's write-up, linked in the sources. What matters for architecture is the rule it implies: a component is dynamic if, and only if, it reads request-time data (cookies(), headers(), searchParams, or an uncached fetch) and sits outside a cached boundary.
What it buys you, measured
The concrete win is time to first byte. When the whole route is dynamic, TTFB waits on your origin and your database. With PPR the shell is static, so TTFB equals the distance from the user to the nearest edge node, not the speed of your slowest query. One production write-up measured a route dropping from 850ms to under 100ms TTFB after moving the shell static (source below). Your numbers will differ. The mechanism does not: static-from-edge beats dynamic-from-origin on first byte every time, and first byte is what Core Web Vitals and impatient users both measure. We covered the wider budget in our Core Web Vitals playbook.
How to structure SaaS routes around PPR
This is where the model earns its place. A SaaS page is rarely all-static or all-dynamic. It is a static frame around a few personalized cells. Structure the route to match.
Push the dynamic boundary as deep as it goes. Do not wrap the whole page body in one Suspense. Wrap the smallest subtree that actually reads request data. A dashboard shell, sidebar, and empty card grid can all be static; only the numbers inside each card are dynamic. The more of the tree you keep in the static shell, the more of the page appears instantly.
Cache the shared, personalize the rest. Marketing pages, docs, pricing, and the logged-out surface are static shells with near-zero dynamic content. App routes invert that: static chrome around dynamic cells. We already argued for treating the marketing site and the app as different architectures. PPR lets one framework serve both without switching rendering modes.
Give every hole a real fallback. The Suspense fallback is not a spinner tax. It is part of the static shell, so it ships instantly and defines the layout. Use skeletons that match the final size of the content, or the shell reflows when data lands and you fail Cumulative Layout Shift.
Watch what you put in the layout. A single cookies() read in a root layout, outside a boundary, pulls the whole route back into dynamic rendering. That is the most common way teams opt out of PPR by accident. Read auth and locale inside a bounded component, not at the top.
Keep streaming observability on. PPR only pays off if you can see the dynamic parts land. Track TTFB separately from full load, and watch the tail: a slow dynamic hole no longer blocks the shell, but it still blocks the data the user came for.
When not to use it
PPR is not free complexity. Skip it when the whole route is genuinely dynamic, a personalized feed where nothing is shared, because there is no static shell to extract and you add config for no gain. Skip it for fully static content (a blog post, a docs page) that was already fast without it. And treat the opt-in as a migration, not a flag flip: turning on cacheComponents makes everything dynamic by default, so an existing app needs each cached surface marked with 'use cache' before it behaves the way it did. On a large App Router codebase that is real work, done route by route. Pair it with the changes we listed in what changed in Next.js 16 and React 19.
The short version: PPR turns "static or dynamic" from a per-page verdict into a per-component decision. For SaaS, where most pages are a static frame around a little live data, that is the rendering model the App Router should have had from the start. In Next.js 16 it finally ships as the default path, not an experiment.
Sources
Frequently asked questions
Is Partial Prerendering stable in Next.js 16?
Yes. PPR shipped stable in Next.js 16 (October 2025). The experimental.ppr flag and the experimental_ppr route segment config were removed. You now enable it through Cache Components by setting cacheComponents: true in next.config.ts, which also unlocks the use cache directive. Earlier versions (14 and 15) kept it behind an experimental flag, so code written for those needs the config update.
What is the difference between Partial Prerendering and streaming SSR?
Streaming SSR renders the whole page on the server per request and streams it as it goes, so the first byte still waits on the server. PPR prerenders a static shell at build time and serves it from the CDN edge, then streams only the dynamic holes from the origin. So PPR gives you instant layout for every visitor plus streaming for the parts that need it. It uses Suspense the same way streaming does, but the shell around those boundaries is already built.
Do I have to rewrite my app to turn on Partial Prerendering?
Not rewrite, but migrate. Enabling cacheComponents flips the default: every route becomes dynamic unless you mark cached data with use cache and bound dynamic reads in Suspense. On a small app that is an afternoon. On a large App Router codebase, do it route by route and watch for request-time reads (cookies, headers) sitting in layouts, since one of those opts the whole route out of the static shell.
Does Partial Prerendering need Vercel?
No. PPR is a Next.js feature, not a Vercel-only one. The static shell is standard prerendered HTML and the dynamic parts stream over a normal chunked HTTP response, so any host that runs Next.js 16 can serve it. You get the full TTFB benefit when a CDN sits in front to serve the static shell from the edge, which you can set up on Cloudflare, a self-hosted CDN, or your own reverse proxy. Without an edge cache the shell still ships first, just from your origin.
Related articles
Studio
Start a project.
One partner for the whole build. Faster delivery, a modern stack, lower cost.