Topic 19 of 47
PPR
Overview
Partial Prerendering (PPR) is a Next.js 14+ feature that combines static and dynamic rendering at component granularity within a single route — instantly serving cached static shells while streaming dynamic content.
Syntax
tsx
// next.config.ts — enable PPR (experimental)
const config = {
experimental: { ppr: "incremental" },
};
// page.tsx — opt in per-route
export const experimental_ppr = true;
// The page shell is static (instant), dynamic parts stream in
import { Suspense } from "react";
export default function ProductPage({ params }) {
return (
<div>
{/* STATIC — served instantly from CDN cache */}
<ProductShell slug={params.slug} /> {/* layout, nav */}
{/* DYNAMIC — streams in after static shell */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice slug={params.slug} /> {/* real-time price */}
</Suspense>
<Suspense fallback={<StockSkeleton />}>
<StockStatus slug={params.slug} /> {/* real-time stock */}
</Suspense>
</div>
);
}Common Pitfalls
- PPR requires the component wrapped in Suspense to contain the dynamic fetch — not the Suspense boundary itself.
- PPR is opt-in per route with experimental_ppr = true — it won't affect existing routes.
- Interview tip: PPR is Next.js's answer to the static vs dynamic dichotomy — it eliminates the choice by allowing both in one page.
Real-World Example
Product page with static layout and dynamic pricing
example
tsx
// The product description, images, and layout are STATIC
// The price, stock, and personalized recommendations are DYNAMIC
// Static part — rendered at build, served from CDN
async function ProductShell({ slug }) {
const product = await fetch(`/api/products/${slug}`, {
cache: "force-cache", // static fetch
}).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<img src={product.image} alt={product.name} />
</div>
);
}
// Dynamic part — bypasses cache, always fresh
async function DynamicPrice({ slug }) {
const pricing = await fetch(`/api/pricing/${slug}`, {
cache: "no-store", // dynamic fetch — breaks static cache
}).then(r => r.json());
return (
<div>
<span className="price">{pricing.formatted}</span>
{pricing.discount && <Badge>Sale {pricing.discountPct}% off!</Badge>}
</div>
);
}