Next.js 15 partial prerendering (PPR) pays off on pages that are mostly static with one or two dynamic regions: the static shell is served immediately and the dynamic parts stream in behind Suspense boundaries[1][2]. It bites when dynamic content fills most of the screen, or when those boundaries are drawn in the wrong places.
In Next.js 15, PPR is experimental and you can adopt it one route at a time, which keeps the risk contained[3][7]. Next.js 16 removed the experimental flag and folded PPR into Cache Components, so if you’re on 16, read the last section before you copy any config from this post.
What PPR does on a real page
Picture a landing page where the hero and testimonials are the same for everyone, but the pricing panel depends on the visitor’s segment or location. You want the shared parts cached and delivered fast everywhere, and you can’t safely cache one pricing panel for every visitor. Partial prerendering ships the prerendered hero and testimonials right away, then streams in the pricing panel as soon as its data is ready. First paint comes sooner, and the static parts aren’t re-rendered on every request, a saving that adds up at scale[1][4].
The mechanism is React’s <Suspense>. You wrap each dynamic component in a boundary and give it a fallback. Next.js prerenders the static parts along with those fallbacks, at build time or when the page is revalidated, then swaps in the dynamic content when it’s ready[1][2][4].
When Next.js 15 PPR shines
PPR works best when a page is mostly static and its dynamic regions can arrive a moment later, like a stock-status box on a product page or a banner that changes by location. Visitors see real content right away and the dynamic parts fill in shortly after. Online stores are full of pages like this: on Cypress Clothiers, a headless Shopify store I built with a Next.js front end, static product pages and a dynamic cart live side by side.
It also helps with a global audience. On a host that supports it, the static shell is cached at the edge, close to visitors, and only the dynamic regions go back to your origin[1][6].
And it suits teams that change pages often. Clearly bounded regions let you change each region’s data strategy on its own, with less of the all-or-nothing coupling that turns small edits into regressions. Keeping stable layout apart from volatile data also makes routes easier to read and safer to refactor.
When PPR bites
Everything depends on where the boundaries go. Poorly placed Suspense boundaries lead to awkward loading states or unexpected client-side rendering. Calling useSearchParams without a boundary, for example, opts the entire page into client-side rendering, which can leave it blank until the JavaScript has loaded[5].
Library support is uneven. Some data libraries label Suspense for data fetching as experimental (TanStack Query’s v4 docs do), which can cause edge cases if you rely on them heavily[8]. PPR works without them. If your app depends on one, pin its version and isolate the components that use it, then cover those components with integration tests and error boundaries.
Monitoring gets harder too. Parts of the page arrive later, so a single load-time number no longer describes what visitors see.
SSR, ISR and PPR side by side
Each mode fits a different kind of page.
| Mode | Best for | Benefits | Trade-offs |
|---|---|---|---|
| SSR | Highly personalized or real-time data on every request | Accurate, request-specific content; simple mental model | Higher infrastructure cost at scale; slower first byte during peaks |
| ISR | Content that’s mostly static with controlled staleness windows | CDN-level speed; amortized compute costs | Staleness risk within the revalidate window; cache invalidation considerations[9] |
| PPR | Mixed pages: static shell + dynamic sections | Faster perceived load without making the whole page dynamic; incremental adoption in Next.js 15[1][3] | Requires careful Suspense boundaries and more monitoring; library compatibility varies[2][8] |
ISR usually has the fastest first byte, because a fresh page is already cached and goes out without any rendering[9]. PPR is close behind: its prerendered shell is sent immediately and the dynamic parts stream into the same response[7]. SSR has to wait for the page’s data before the first byte. Streaming with loading.js or Suspense lets it send a layout sooner[4], but that layout is still rendered on every request.
Drawing Suspense boundaries
A good Suspense boundary wraps one cohesive dynamic unit, with a fallback that holds the layout steady. Think of a RecommendationsGrid whose placeholder keeps every card at its final size. Avoid wrapping half of a hero banner or grouping unrelated regions under one boundary, since both make the page jump as content arrives.
Hooks that read the URL need the same care. Wrap the smallest component that calls useSearchParams in its own boundary, so the rest of the page stays in the static shell and avoids a full client-side rendering bailout[5]. React’s documentation explains how Suspense shows a fallback and then reveals the finished UI once the data has loaded[2].
A decision tree for SSR, ISR and PPR
The choice mostly comes down to how long the content stays valid and how much of it is specific to each visitor. Use this flow as a starting point, then adjust it for how much staleness you can accept and how quickly your data sources respond.
A page route
Is most of the content stable for minutes or longer?
No
Use SSR
Speed up the data source and cache what you can upstream
- Yes
Does user-specific content have to appear instantly?
No
Use ISR
Set the revalidate window to how fresh the page must be
- Yes
Can the dynamic parts sit behind Suspense boundaries?
No
Use SSR
- Yes
Use PPR
A static shell, with the dynamic parts streamed in
Real pages often fit more than one branch. Product pages with shared stock levels might do well with ISR plus on-demand revalidation for best sellers, while signed-in dashboards often suit PPR: the navigation and overview cards render immediately and the data-heavy widgets stream in.
As a rule of thumb:
- Choose ISR when some staleness is acceptable and the content is the same for everyone. It’s the fastest option and uses the least compute.
- Choose SSR when every request needs its own data immediately and you can afford to keep backend latency low.
- Choose PPR when you want a fast, cacheable shell with dynamic inserts that don’t hold up first paint. The more of the viewport the stable part covers, the more PPR tends to pay off.
Rolling PPR out in Next.js 15
I’d start small. In Next.js 15, setting experimental.ppr to 'incremental' lets you opt individual layouts and pages into PPR with a route segment export, so you can try it on a few high-impact routes and get used to drawing boundaries before you convert complex ones[3][7].
// Next.js 15 (canary): next.config.js
module.exports = {
experimental: {
ppr: 'incremental',
},
}
// Next.js 15: app/pricing/page.tsx opts in to PPR
export const experimental_ppr = true
Put real work into fallbacks. A layout shift or a spinner that looks broken is the quickest way to make a PPR page feel worse than the version it replaced. Use skeletons that keep the final height and make it obvious something is loading.
Then measure each stage. Keep tracking first byte and first paint, and add the time each key region finishes loading, because with streaming “done” is a series of moments. Check that caching and fallbacks behave the same across regions and devices.
What changed in Next.js 16
Next.js 16 removed both experimental.ppr and the experimental_ppr route segment config. PPR is now part of Cache Components: you turn on the top-level cacheComponents option, and PPR becomes the default rendering behavior in the App Router.
// Next.js 16: next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Switching takes more than renaming a flag. With Cache Components, data fetching is dynamic by default, and caching is opt-in through the use cache directive and cacheLife, which replace the dynamic, revalidate and fetchCache segment configs. In the decision tree above, the ISR revalidate window becomes a cacheLife setting. Uncached data outside a Suspense boundary can also fail the build.
The Next.js 16 upgrade guide also says PPR in 16 works differently from the Next.js 15 canaries. It tells anyone using PPR today to stay on their current canary, and points to the Migrating to Cache Components guide for the move. You can still convert one route at a time: a codemod removes the experimental_ppr exports, and export const instant = false opts a segment out of validation until you get to it. The rest of the upgrade is in Next.js 16 vs 15: breaking changes and how to fix them.
The trade-off this post describes is the same in both versions. PPR earns its place when most of the screen can be prerendered and the personal parts can wait a moment behind a Suspense boundary: you get a faster first impression than SSR without forcing those parts into a cache. When dynamic content fills the viewport, SSR is simpler, and when nothing on the page is personal, ISR is hard to beat.
Sources
Next.js Docs. (2025). Getting Started: Partial Prerendering (App Router). Retrieved from https://nextjs.org/docs/app/getting-started/partial-prerendering
React Docs. (2025). <Suspense> – React. Retrieved from https://react.dev/reference/react/Suspense
Next.js Docs. (2025). next.config.js: ppr (incremental adoption). Retrieved from https://nextjs.org/docs/app/api-reference/config/next-config-js/ppr
Next.js Docs. (2025). File-system conventions: loading.js (Streaming with Suspense). Retrieved from https://nextjs.org/docs/app/api-reference/file-conventions/loading
Next.js Docs. (2025). Missing Suspense boundary with useSearchParams. Retrieved from https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
Vercel Blog. (2023).
Partial prerendering: Building towards a new default rendering model for web applications
. Retrieved from https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model
Next.js Blog. (2024). Next.js 15 RC (Incremental adoption of PPR). Retrieved from https://nextjs.org/blog/next-15-rc
TanStack Query Docs. (2025). Suspense (Experimental). Retrieved from https://tanstack.com/query/v4/docs/react/guides/suspense
Vercel Docs. (2025). Incremental Static Regeneration (ISR). Retrieved from https://vercel.com/docs/incremental-static-regeneration
Written by
Founder and engineer at Blue Nebula, a one-person studio in Daly City, CA, that builds web apps, Shopify stores and AI features. Top Rated Plus on Upwork with 3,700+ hours of client work.
More about me