QUICK START:HTMLCSSJavaScriptTypeScriptPythonSQLReactNext.jsNode.jsLinux & UbuntuKotlinSwiftC# / .NETJavaGoRustC++DSASystem DesignDevOpsCybersecurityAI / ML
Advanced 24 min readModule: Module 13: Partial Prerendering (PPR): Static Shells & Dynamic Holes

Partial Prerendering (PPR) & Hybrid Edge Streaming

Eliminate the choice between Static Site Generation (SSG) and Server-Side Rendering (SSR) using Partial Prerendering (PPR) in Next.js: instant static HTML shells with embedded streaming dynamic holes.

What You Will Learn in This Lesson

  • The architectural revolution of PPR: Instant 0ms TTFB static shell + streamed dynamic user data
  • Enabling Partial Prerendering with `experimental.ppr = 'incremental'`
  • Defining static boundaries and wrapping dynamic cookies/headers inside `<Suspense>` holes
  • How Edge CDNs serve the pre-rendered static shell while the origin server streams dynamic chunks

Introduction & Core Concept

Historically in web development, developers were forced to make a binary choice per page: either make it completely Static (fast TTFB from CDN, but cannot show user-specific data) or completely Dynamic (slow TTFB from origin server, blocking initial render). Partial Prerendering (PPR) combines both: the static shell (navbars, hero sections, layouts) is pre-rendered at build time and served in 0ms from the CDN, while dynamic content (user carts, live prices) is streamed into embedded Suspense holes within the same HTTP response.
WHY DOES THIS MATTER IN THE REAL WORLD?

PPR delivers sub-20ms Time-To-First-Byte (TTFB) and perfect Core Web Vitals (LCP) for e-commerce and SaaS dashboards while still rendering live, authenticated user data.

Syntax & Structure

typescript
export const experimental_ppr = true;
<Suspense fallback={<CartSkeleton />}>
<DynamicUserCart />
</Suspense>

Implementing Partial Prerendering (PPR) on an E-Commerce Page

typescript
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// app/courses/[slug]/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
// 1. Enable Partial Prerendering for this route
export const experimental_ppr = true;
// 2. Dynamic Component (Reads cookies -> Rendered on-demand at request time)
async function UserEnrollmentStatus({ courseId }: { courseId: string }) {
const cookieStore = await cookies();
const sessionToken = cookieStore.get("session_token")?.value;
// Simulate database user lookup
await new Promise(r => setTimeout(r, 400));
const isEnrolled = !!sessionToken;
return (
<div className="p-4 rounded-lg bg-blue-900 text-blue-100 font-semibold">
{isEnrolled ? "✅ Enrolled — Continue Lesson" : "⚡ Enroll Now for Free"}
</div>
);
}
// 3. Page Component: Static Shell Pre-rendered at Build Time!
export default async function CourseDetailPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return (
<main className="p-8 max-w-4xl mx-auto space-y-6">
{/* STATIC SHELL: Instant 0ms TTFB from CDN */}
<header className="border-b pb-4">
<h1 className="text-3xl font-bold">Course: {slug.toUpperCase()}</h1>
<p className="text-slate-400">Master enterprise software engineering with KWAS Academy.</p>
</header>
{/* DYNAMIC HOLE: Streamed into HTML response over HTTP */}
<Suspense fallback={<div className="h-14 bg-slate-800 animate-pulse rounded-lg" />}>
<UserEnrollmentStatus courseId={slug} />
</Suspense>
</main>
);
}

Line-by-Line Technical Breakdown

1Under the Hood: At build time, Next.js generates a static HTML file containing the pre-rendered shell and fallback HTML. When a request arrives, the Edge CDN sends the static shell immediately and initiates a background stream to resolve the pending Suspense promises, replacing fallback markup in-place.

Try It Yourself (Interactive Editor)

Modify the code in real-time and click Run to test live browser output and console logs.

Intelligent Code Runner & Live Sandbox[TYPESCRIPT]
TYPESCRIPT SOURCE EDITOR
Interactive Live Code

Common Mistakes & How to Avoid Them

#1: Accessing cookies() or searchParams at the root level of the page outside a Suspense boundary in PPR.

Reading dynamic functions like `cookies()` or `headers()` outside `<Suspense>` opts the entire page into dynamic rendering, destroying the static shell.

Incorrect / Antipattern
export default async function Page() { const c = await cookies(); return <div/>; }
Correct / Professional Solution
export default async function Page() { return <Suspense><DynamicChild/></Suspense>; }

Industry Best Practices & Professional Standards

  • Enable `experimental: { ppr: 'incremental' }` in `next.config.ts`.
  • Keep dynamic data accesses (`cookies()`, `headers()`) isolated inside `<Suspense>` boundaries.
  • Provide accurate skeleton fallbacks in Suspense to prevent Cumulative Layout Shift (CLS).

Lesson Summary & Core Takeaways

  • PPR merges Static Site Generation with dynamic Server-Side streaming.
  • Static shell renders instantly from the Edge CDN in 0ms TTFB.
  • Dynamic user data streams seamlessly into Suspense holes over the same connection.