Partial Prerendering (PPR)
Partial Prerendering is a rendering model introduced as an experimental feature in Next.js 14 and refined through the Next.js 15 release line. It attacks a problem that dynamic-vs-static rendering traditionally forces you into an all-or-nothing choice for: a single route either renders entirely statically, or the moment any part of it needs per-request data, the whole route becomes dynamic. PPR lets one route be both at once.
The core idea: a static shell with dynamic holes
With PPR, Next.js prerenders a static "shell" of the page at build time — everything that does not depend on the incoming request — and serves that shell instantly, the same way a fully static page would. Any part of the page that genuinely needs per-request data (cookies, headers, a personalized fetch, searchParams) is wrapped in a React Suspense boundary. Those boundaries are left as "holes" in the static shell, and Next.js streams the real content into each hole as soon as its data is ready — all within the same route, in the same response.
A worked example: a product page
Imagine a product page. The product name, price, description, and images are the same for everyone and known at build time — pure static content. But a "Recommended for you" section depends on the signed-in visitor and has to be computed per request.
app/products/[id]/page.tsx
import { Suspense } from 'react'
import { ProductDetails } from './ProductDetails'
import { Recommendations } from './Recommendations'
import { RecommendationsSkeleton } from './RecommendationsSkeleton'
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
// Static: no request-specific data, can be prerendered.
const product = await getProduct(params.id)
return (
<div>
<ProductDetails product={product} />
{/* Dynamic hole: streams in after the static shell is served. */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={params.id} />
</Suspense>
</div>
)
}The ProductDetails section renders instantly from the prerendered shell — visitors see real content immediately, just like a fully static page. The Recommendations component, which internally reads cookies or a session to personalize its data, streams in moments later inside its Suspense boundary, without ever forcing ProductDetails to wait for it.
Why this matters
You get the instant response time of static generation for the majority of a page.
You still get genuinely dynamic, personalized, or per-request content in the same route — no separate page, no client-side fetch required for it.
It removes the old trade-off where one dynamic fragment (e.g. a personalized banner) forced an entire otherwise-static page into fully dynamic rendering.
Rendering model | What happens |
|---|---|
Fully static | Whole route prerendered at build time; identical for every visitor |
Fully dynamic | Whole route rendered per request, even for parts that never change |
Partial Prerendering | Static shell served instantly; Suspense-wrapped dynamic sections stream in on top of it |
export const experimental_ppr = true per route) and has continued to mature through the Next.js 15 line. Its exact configuration, defaults, and stability level are still changing release to release — always check the current Next.js documentation for the latest status before relying on it in production.