Partial Prerendering
The idea in one sentence
Section titled “The idea in one sentence”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 holeimport { 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.
Enabling PPR in Next.js 16
Section titled “Enabling PPR in Next.js 16”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.
Contrast with all-static and all-dynamic
Section titled “Contrast with all-static and all-dynamic”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]