NextjsPartial Prerendering (PPR)

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

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

Status: experimental and evolving
PPR shipped as an experimental, opt-in feature starting in Next.js 14 (enabled via an experimental flag in next.config.js, plus an explicit 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.
Tip
Think of PPR as "make the static parts of a page as fast as possible, and let Suspense own the parts that genuinely cannot be known ahead of time." If a section does not depend on the request, keep it outside any Suspense boundary so it stays part of the static shell.