Master Partial Prerendering (PPR) in Next.js. Learn how to combine static edge shells with dynamic React Suspense streaming for optimal Core Web Vitals.
For years, web architecture forced teams into a binary tradeoff. Static Site Generation (SSG) delivers sub-50ms Time to First Byte (TTFB) directly from edge CDNs, but it fails whenever personalized user state, localized inventory tallies, or authenticated sessions are required. Conversely, full Dynamic Server-Side Rendering (SSR) handles personalized data effortlessly, but every incoming request incurs server cold starts, database query waterfalls, and elevated TTFB that degrade Core Web Vitals across the board.
When engineering data-dense internal tools like [Inventory Management / InvMaster OS](/projects/1), relying strictly on pure SSR leads to noticeable latency when loading warehouse overview screens. Conversely, pure SSG fails because stock levels shift by the second across multi-godown networks. Next.js Partial Prerendering (PPR) resolves this fundamental tension by eliminating the page-level rendering constraint.
At build time, Next.js inspects the route tree and identifies static nodes versus dynamic leaves guarded by React Suspense boundaries. The compiler produces a lightweight pre-rendered static HTML shell containing the application layout, navigation bars, CSS assets, and placeholder skeleton markup defined in `<Suspense fallback={...}>`.
When a user requests the route, the edge network immediately dispatches the pre-rendered shell. Concurrently, the server runtime executes the asynchronous dynamic Server Components wrapped inside Suspense boundaries. As these promises resolve, the rendered HTML chunks stream through the open HTTP/2 or HTTP/3 stream into their respective DOM slots without triggering a secondary roundtrip or client-side layout thrashing.
import { Suspense } from 'react';
import { ShellHeader } from '@/components/ShellHeader';
import { GodownMetricsSkeleton } from '@/components/skeletons/GodownMetricsSkeleton';
import { RealtimeStockTicker } from '@/components/RealtimeStockTicker';
import { WarehouseAllocationGrid } from '@/components/WarehouseAllocationGrid';
// PPR flag activated at the segment level
export const experimental_ppr = true;
export default function WarehouseDashboardPage() {
return (
<main className="min-h-screen bg-slate-950 p-6 text-slate-100">
{/* Static Shell: Pre-rendered at build time, delivered sub-30ms via edge */}
<ShellHeader
title="Multi-Godown Stock Control"
description="Real-time FEFO batch allocations and inventory movement."
/>
{/* Dynamic Slot 1: Streams real-time warehouse count and low-stock telemetry */}
<section className="my-8">
<Suspense fallback={<GodownMetricsSkeleton />}>
<RealtimeStockTicker />
</Suspense>
</section>
{/* Dynamic Slot 2: Renders authenticated user permissions and location routing */}
<section>
<Suspense fallback={<div className="h-96 animate-pulse rounded-lg bg-slate-900" />}>
<WarehouseAllocationGrid />
</Suspense>
</section>
</main>
);
}To prevent an unintentional dynamic opt-out across the entire page, dynamic runtime primitives—such as reading request cookies, accessing request headers, or querying un-cached dynamic database tables—must be encapsulated strictly within the Suspense boundary. If dynamic functions are called outside a Suspense wrapper, Next.js de-optimizes the entire route back to traditional server-side rendering.
import { cookies } from 'next/headers';
import { db } from '@/lib/database';
export async function RealtimeStockTicker() {
// Dynamic runtime reads: Isolated inside Suspense
const cookieStore = await cookies();
const activeWarehouseId = cookieStore.get('active_warehouse_id')?.value ?? 'central-hub';
// Database query triggered on request, streamed directly to edge shell
const inventorySummary = await db.stock.aggregate({
where: { godownId: activeWarehouseId },
_sum: { currentQuantity: true },
_count: { expiringBatches: true }
});
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-5 rounded-xl border border-slate-800 bg-slate-900/60">
<span className="text-xs font-mono uppercase text-emerald-400">In-Stock Units</span>
<p className="text-3xl font-bold tracking-tight text-white mt-1">
{inventorySummary._sum.currentQuantity?.toLocaleString() ?? 0}
</p>
</div>
<div className="p-5 rounded-xl border border-slate-800 bg-slate-900/60">
<span className="text-xs font-mono uppercase text-amber-400">FEFO Expiry Warnings</span>
<p className="text-3xl font-bold tracking-tight text-white mt-1">
{inventorySummary._count.expiringBatches}
</p>
</div>
</div>
);
}By shifting the initial response off the Node.js/Edge compute runtime and directly onto the CDN edge cache, PPR cuts TTFB down to edge network latencies (consistently below 40ms in domestic routing). Simultaneously, Largest Contentful Paint (LCP) accelerates because browser layout engines immediately receive styles and critical structural HTML. The remaining data fetching runs asynchronously without choking the main thread or postponing initial paint.
Transitioning legacy SSR applications or complex enterprise dashboards to modern partial prerendering requires careful boundary isolation, database connection pooling, and layout optimization. As an independent web developer, I engineer fast, resilient web applications tailored for real-world load. Explore my [Full-Stack Web Development Services](/services) or [get in touch for a consultation](/contact) to audit and optimize your Next.js architecture.
ISR operates on an all-or-nothing page level: either the whole page is served from the static cache, or the entire route undergoes regeneration in the background. In contrast, PPR unifies static and dynamic rendering within the exact same route. The layout and static UI are returned instantaneously from the edge cache, while personalized or real-time dynamic segments stream directly into React Suspense holes without re-rendering static shells.
No. PPR is incremental and built on top of native React Suspense. Any standard React Server Component wrapped in a Suspense boundary with a static fallback will automatically become a streamed dynamic hole, while the surrounding layout, navigation, and static content compile into the prerendered shell.
Search engine crawlers that do not support streaming or JavaScript execution receive the static HTML shell immediately with status 200. Crawlers supporting modern streaming (such as Googlebot) consume the streamed chunks as the Suspense boundaries resolve, ensuring that critical metadata and crawlable body content remain fully indexed.