Partial Prerendering (PPR)
Static shells with dynamic holes.
The Best of Both Worlds
For years, developers had to choose between Static Site Generation (fast, but generic) and Server-Side Rendering (personalized, but slower).
**Partial Prerendering (PPR)** merges them. During the build step, Next.js generates a static HTML shell of your page. However, it leaves 'holes' wherever it finds a <Suspense> boundary wrapping dynamic data.
Fixing the All-or-Nothing Trap
Before PPR, if a static e-commerce homepage wanted to display the user's name in the header (which requires reading a cookie), the *entire page* was forced to become dynamic, severely harming TTFB.
With PPR, the user receives the static homepage layout instantly from the Edge CDN. A few milliseconds later, the personalized header streams into the <Suspense> hole.
Implementation
To use it in Next.js 15, enable it in config and explicitly opt-in at the route level:
typescript
// app/page.tsx
import { Suspense } from 'react';
import { CartCount, Skeleton } from '@/components';
// Opt into PPR
export const experimental_ppr = true;
export default function Page() {
return (
<main>
<StaticHero />
<Suspense fallback={<Skeleton />}>
{/* This streams in dynamically! */}
<CartCount />
</Suspense>
</main>
);
}
PPR Streaming Visualization
Watch how a static shell is served instantly from the edge, followed by a dynamic payload streaming into the Suspense boundary.
Check yourself
Pick an answer to lock it in, then read why. Getting one wrong is part of how it sticks.
Remember this
- PPR combines SSG (static shell) and SSR (dynamic streaming) on the same page.
- It solves the 'all-or-nothing' rendering dilemma.
- You must wrap dynamic Server Components in
<Suspense>to create the 'holes' for the static shell. - Dynamic functions called outside of Suspense will break the static shell.
Done with this concept?
Mark it complete to track your progress. No login needed.