Partial Prerendering in Next.js has shipped as an experimental, opt-in feature. As the engineer deciding for a production app, how do you weigh adopting it?
answer
- bounded downside, unbounded upside
- it degrades rather than breaks
- the refactor is worth doing anyway
- confirm the platform actually streams
- measure field TTFB, not total load
basics
~20 sWeigh three things: an experimental flag can change shape between releases, the failure mode is graceful because a route simply reverts to normal rendering, and the real cost is restructuring components around Suspense boundaries — a refactor that pays off even without PPR.
solid answer
~50 sI'd start from the fact that the failure mode is unusually kind: if PPR is off or a route can't produce a shell, that route just renders the way it always did. There's no broken page, which makes this a lower-risk experiment than most. Against that, it has lived behind `experimental.ppr` through Next 14 and 15, and experimental surfaces get renamed and reshaped between versions, so I'd budget for the config changing under us and avoid making the app's architecture depend on the flag existing. The real cost isn't configuration, it's structural: every request-time read has to sit under a Suspense boundary with a designed fallback. That refactor is worth doing on its own merits — it's the same shape streaming wants — which is what makes the bet cheap. I'd pilot on two or three high-traffic, mostly-static routes, verify our hosting actually caches the shell and doesn't buffer the stream, and judge on field TTFB rather than a synthetic run.
go deeper
Know that the feature is opt-in and experimental, and that turning it off leaves pages working normally. You are not expected to own the adoption call, but you should not assume experimental means safe by default.
Be able to state the actual cost — restructuring components around Suspense boundaries with real fallbacks — and why that is more than a config change. Explain what reverting looks like.
Show you would pilot narrowly, verify the hosting path serves the shell from cache and streams without buffering, and judge on field time-to-first-byte. Name which routes are poor candidates and why.
Own the risk framing: bounded downside because it degrades rather than breaks, a refactor that retains value if abandoned, and a flag deliberately kept out of application code so a future rename stays a config edit. Have a position on how upgrades are reviewed.
## What you are actually deciding The question is rarely "is PPR good". It is: what does this commit us to, what happens when it changes, and would we regret the work if we backed out? Framing the decision that way turns an argument about a feature into a manageable bet. ## The stability question Partial Prerendering has been an experimental, opt-in feature through Next 14 and 15, configured under `experimental.ppr` and, in incremental mode, an `experimental_ppr` export per route. Anything under an experimental namespace carries an explicit promise of instability: the key can be renamed, its accepted values can change, and the behaviour it selects can be folded into a differently-named model in a later major. The practical rule is to treat the flag as a **deployment detail** that lives in one config file and one export per route, and to never let application code branch on whether PPR is active. If the flag disappears tomorrow, the change should be confined to configuration. ## The failure mode is graceful, and that matters more than it sounds Most experimental features fail loudly — a broken build, a runtime crash, a subtly wrong render. PPR fails by **reverting**. Turn it off, or fail to produce a shell for a route, and that route renders per request exactly as it did before. The pages still work, the data is still correct, users still get the right content. What you lose is a performance optimisation. That asymmetry is the strongest argument for adopting early: the downside is bounded to "we did some refactoring and got less than we hoped", not "we shipped an outage". Being able to articulate that is the difference between reckless and calculated adoption. ## The real cost is architectural, not configuration The config change takes a minute. The work is restructuring components so that every read of request-scoped data sits beneath a `<Suspense>` boundary, with a fallback someone actually designed. On a mature codebase this means auditing layouts, extracting personalized fragments out of large components, and designing skeletons for regions that previously had none. Here is why the bet is cheap anyway: **that refactor is the right shape regardless**. It is exactly what you would do to make a slow region stream independently. If PPR is abandoned, the components are still better factored and the streaming behaviour is still better. Very little of the work is thrown away. Contrast that with an experiment that requires a bespoke abstraction you would have to unwind. ## Does your platform actually deliver the benefit An early, cheap check that teams skip: confirm the deployment target genuinely serves the prebuilt shell from a cache **and** forwards a streamed response incrementally. A reverse proxy, ingress, or compression layer that buffers the body until it is complete erases the entire benefit while the build output happily reports success. Verify this on one route before committing the team to a codebase-wide refactor — it is a day of work that can save a quarter of misplaced effort. ## Which routes are worth it PPR pays where a page is mostly shared with a few personalized regions, and where the shared part is above the fold. It pays little on a dashboard that is personalized end to end, because there is almost no shell. It pays little on a route that is already fully static, because there is nothing dynamic to separate. Sorting your routes by traffic and by the shared/personalized ratio usually shows that a small number of them hold nearly all the available benefit — which is also the argument for incremental adoption rather than a global switch. ## How I would actually run it Pick two or three high-traffic routes with a large shared surface. Turn on incremental mode so nothing else in the app changes. Do the boundary refactor properly, including real fallbacks. Verify the build marks the routes as partially prerendered. Then measure **field** time-to-first-byte and the paint that follows, not a synthetic lab run, because the thing you are testing is whether a cache and a stream behave for real users on real networks. Total load time barely moves — the same work is done either way — so a team measuring that will conclude the feature does nothing. If the field numbers move, expand deliberately. If they do not, you have learned something true about your traffic and your infrastructure at the cost of a refactor you wanted anyway. ## The version discipline to keep Because the surface is experimental, pin the Next version, read the release notes on every upgrade for this specific key, and keep the opt-in narrow enough that a rename is a small edit. The failure you want to avoid is not "PPR changed" — it is "PPR changed and we had 200 routes and no idea which depended on it".
- What would make you decide against adopting it for a given app?An app that is personalized above the fold on nearly every route, because there is no meaningful shell to prebuild. A hosting path that buffers responses, because the benefit never reaches users. Or a team already at capacity, since the cost is a real refactor across layouts and components, not a config toggle, and half-done boundary work delivers nothing.
- How do you keep an experimental flag from becoming an architectural dependency?Confine it to configuration — the config key and a per-route export — and never let application code branch on whether it is active. Components should be structured so they behave correctly either way. Then a rename or removal in a future major is a small mechanical edit rather than an unwinding of the codebase.
- Why measure field TTFB rather than total page load when evaluating the pilot?Because the same total work happens either way; PPR reorders when bytes leave the server rather than reducing them. Total load time therefore barely moves, and a team judging on it concludes the feature does nothing. The change shows up in time to first byte and the first paint that follows, on real networks with the real cache in play.
saying these in an interview costs you the question
- Treats it as a config toggle with no code cost
- Ignores that experimental flags get renamed between versions
- Rolls it out app-wide before validating on a few routes
- Judges the pilot on total page load time
- Assumes every route benefits regardless of how personalized it is