Skip to content

Partial Prerendering

Partial Prerendering serves a prerendered static shell instantly and streams the dynamic “holes” — each wrapped in Suspense — into that shell at request time, giving one route the speed of static and the freshness of dynamic.

One route, static shell plus dynamic holes

Section titled “One route, static shell plus dynamic holes”

Before PPR you picked one mode per route: fully static (fast, but no per-request data) or fully dynamic (fresh, but nothing prerendered). PPR removes the either/or. The parts of the page that never change are prerendered into a static shell; the parts that need request-time data become dynamic holes, each behind a <Suspense> boundary, streamed in as they resolve.

// app/product/[id]/page.tsx — static shell, dynamic price streamed into a hole
import { Suspense } from 'react';
import { cookies } from 'next/headers';
async function LivePrice() {
const store = await cookies();
const currency = store.get('currency')?.value ?? 'USD';
const price = await fetch('https://api.example.com/price', {
cache: 'no-store',
}).then((r) => r.json());
return <strong>{price.amount} {currency}</strong>;
}
export default function ProductPage() {
return (
<main>
<h1>Wireless Headphones</h1>
<p>This description is prerendered into the static shell.</p>
<Suspense fallback={<span>Loading price…</span>}>
<LivePrice />
</Suspense>
</main>
);
}

The heading and description ship instantly as static HTML; LivePrice is the dynamic hole that streams in per request.

In Next.js 16, Partial Prerendering ships as part of Cache Components and is turned on with a single config flag. The old experimental ppr flag and the route-level experimental_ppr export have been removed — you no longer opt in per route.

// next.config.ts — enable Partial Prerendering (Next.js 16)
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;

With cacheComponents: true, any route that mixes prerendered content with Suspense-wrapped dynamic content is partially prerendered automatically. Because this area is still stabilizing, verify the current status and naming in the Next.js docs before you rely on it in production.

The three models line up along one axis — how much of the route is known at build time:

  • All static — the entire route is prerendered; nothing is per-request. Fastest, but no fresh data.
  • All dynamic — the entire route renders on each request. Always fresh, but nothing is prerendered, so TTFB waits on the slowest data.
  • PPR — the shell is prerendered like static, and only the holes are dynamic. Fast first paint and per-request data in the same route.
flowchart LR
  A[Request] --> B[Static shell served instantly]
  B --> E[Shell prerendered at build time]
  B --> C[Dynamic hole in Suspense]
  C --> D[Streamed at request time]
A PPR route: static shell with a streamed dynamic hole
What does Partial Prerendering combine in a single route?
How is each dynamic hole in a PPR route delivered?
How do you enable PPR in Next.js 16?
How does PPR differ from an all-dynamic route?