Partial Pre-Rendering (PPR) is Next.js's answer to the long-standing trade-off between static speed and dynamic content. By pre-rendering a static shell at build time while streaming dynamic content on request, PPR delivers the best of both worlds: instant page loads and personalized, real-time data. This is the rendering strategy that could become the default for modern web applications.
The Problem PPR Solves
For years, web developers have faced an uncomfortable choice:
Option A: Go Static (SSG)
- Lightning-fast page loads
- Excellent SEO
- No personalization or real-time data
- Stale content until you rebuild
Option B: Go Dynamic (SSR)
- Fresh, personalized content
- Real-time data
- Slower initial page loads
- Higher server costs
Option C: Hybrid (ISR)
- Mostly static with periodic updates
- Better than pure SSG for changing content
- Still can't personalize
- Complex to reason about
Every approach required trade-offs. A single personalized element—like a user's cart count or logged-in status—could force an entire page into dynamic rendering, sacrificing the speed benefits of static generation.
Partial Pre-Rendering eliminates this trade-off.
What is Partial Pre-Rendering?
PPR allows you to render most of a page statically at build time while leaving "holes" for dynamic content that streams in on request.
The Key Concept
Think of your page as having two types of content:
- Static Shell: Header, navigation, footer, layout, product descriptions, marketing copy—anything that's the same for every user
- Dynamic Holes: User-specific data, personalization, real-time information, shopping carts, recommendations
With PPR:
- The static shell is pre-rendered at build time and served instantly from the edge
- Dynamic holes are streamed from the server as they become ready
- Users see meaningful content immediately, with dynamic parts filling in seamlessly
How It Differs from Previous Approaches
| Approach | When HTML is Generated | What's Generated |
|---|---|---|
| SSG | Build time | Entire page (static) |
| SSR | Every request | Entire page (dynamic) |
| ISR | Build time + revalidation | Entire page (cached, refreshed) |
| PPR | Build time + request time | Static shell (build) + dynamic holes (request) |
PPR is the first approach that truly combines static and dynamic rendering within the same page.
How PPR Works Under the Hood
The Build Process
When you build a Next.js application with PPR enabled:
- Next.js identifies which parts of your page are static and which require request-time data
- Static portions are rendered to HTML and cached
- Dynamic portions (wrapped in
<Suspense>) are marked as "holes" with fallback content - The static shell with fallback placeholders is stored for instant delivery
The Request Flow
When a user visits a PPR-enabled page:
1. User requests page
↓
2. Edge server instantly returns static shell (HTML)
↓
3. Browser renders static content immediately
↓
4. Server begins streaming dynamic content
↓
5. Dynamic holes are filled as data arrives
↓
6. Page is complete
The critical insight: Steps 3-5 happen in parallel. Users see and interact with static content while dynamic content loads.
Single HTTP Request
Unlike traditional approaches where dynamic content might require additional client-side fetches, PPR sends everything—static HTML and streamed dynamic parts—in a single HTTP response. This eliminates extra round trips and improves overall performance.
The Performance Impact
Core Web Vitals Improvements
PPR directly improves the metrics Google uses to evaluate user experience:
Largest Contentful Paint (LCP)
- Static content loads instantly from the edge
- Users see meaningful content in hundreds of milliseconds
- No waiting for server-side data fetching to complete
First Input Delay (FID) / Interaction to Next Paint (INP)
- Reduced JavaScript execution on the client
- Static HTML is immediately interactive
- Hydration is faster with less client-side work
Cumulative Layout Shift (CLS)
- Suspense boundaries with proper fallbacks prevent layout shifts
- Content dimensions are preserved during streaming
- Smoother visual experience
Real-World Numbers
According to Vercel's benchmarks:
- Static delivery: Served from the edge in under 100ms globally
- Time to First Byte: Comparable to pure static sites
- Dynamic content: Streams immediately after static shell, no separate requests
For a typical e-commerce product page:
- Without PPR (full SSR): 800-1200ms to first meaningful content
- With PPR: 100-200ms to static shell, dynamic content streams in parallel
When to Use PPR
Ideal Use Cases
E-commerce Product Pages
- Static: Product description, images, specifications, reviews summary
- Dynamic: Personalized price, inventory status, user's cart, recommendations
News and Content Sites
- Static: Article content, author info, related articles
- Dynamic: User's reading history, personalized recommendations, comments
Dashboards with Mixed Content
- Static: Layout, navigation, static widgets
- Dynamic: User-specific data, real-time metrics, notifications
Marketing Pages with Personalization
- Static: Hero section, features, testimonials, pricing
- Dynamic: User's name, account status, personalized CTAs
Social Platforms
- Static: Post content, media, static engagement counts
- Dynamic: User's reactions, personalized feed position, real-time updates
When PPR Might Not Help
- Fully static sites: If you have no dynamic content, pure SSG is simpler
- Fully personalized pages: If everything depends on the user, PPR has nothing to pre-render
- Real-time applications: Live collaboration tools, chat apps where everything is dynamic
Implementing PPR in Next.js 15
Enabling PPR
PPR is currently experimental in Next.js 15. Enable it in your configuration:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
ppr: 'incremental',
},
}
export default nextConfig
The 'incremental' setting lets you opt-in to PPR on a per-route basis.
Opting Routes Into PPR
Add the experimental flag to layouts or pages:
// app/products/[id]/page.tsx
export const experimental_ppr = true
export default function ProductPage({ params }) {
return (
// Your page content
)
}
Defining Dynamic Boundaries
Use React's <Suspense> to mark dynamic content:
import { Suspense } from 'react'
import { ProductInfo } from './ProductInfo'
import { UserCart } from './UserCart'
import { CartSkeleton } from './CartSkeleton'
export const experimental_ppr = true
export default function ProductPage({ params }) {
return (
<main>
{/* Static - pre-rendered at build time */}
<ProductInfo productId={params.id} />
{/* Dynamic - streamed at request time */}
<Suspense fallback={<CartSkeleton />}>
<UserCart />
</Suspense>
</main>
)
}
What Makes Components Dynamic?
Components become dynamic when they access request-time information:
import { cookies } from 'next/headers'
export async function UserCart() {
// Accessing cookies makes this dynamic
const session = (await cookies()).get('session')?.value
const cart = await fetchUserCart(session)
return (
<div>
<span>Cart: {cart.itemCount} items</span>
</div>
)
}
Dynamic triggers include:
cookies()headers()searchParams- Uncached data fetching
- Any request-specific information
The Suspense Boundary Pattern
// ❌ Without Suspense - entire page becomes dynamic
export default async function Page() {
const user = await getCurrentUser() // Dynamic!
const products = await getProducts() // Could be static
return (
<div>
<UserGreeting user={user} />
<ProductList products={products} />
</div>
)
}
// ✅ With Suspense - only UserGreeting is dynamic
export default async function Page() {
const products = await getProducts() // Static, cached
return (
<div>
<Suspense fallback={<GreetingSkeleton />}>
<UserGreeting /> {/* Dynamic, streamed */}
</Suspense>
<ProductList products={products} /> {/* Static */}
</div>
)
}
PPR vs Other Rendering Strategies
PPR vs Pure SSG
| Aspect | SSG | PPR |
|---|---|---|
| Build time content | ✅ Everything | ✅ Static portions |
| Dynamic content | ❌ Not possible | ✅ Streamed |
| Personalization | ❌ None | ✅ In dynamic holes |
| Speed | ⚡ Fastest (fully cached) | ⚡ Nearly as fast |
| Use case | Truly static sites | Dynamic sites wanting static speed |
PPR vs SSR
| Aspect | SSR | PPR |
|---|---|---|
| Initial response | Waits for all data | Instant static shell |
| Time to first byte | Slower (data dependent) | Fast (static cached) |
| Server load | Every request, full render | Only dynamic portions |
| Edge delivery | Limited | Static shell from edge |
| Use case | Fully dynamic pages | Mostly static with dynamic parts |
PPR vs ISR
| Aspect | ISR | PPR |
|---|---|---|
| Content freshness | Periodic revalidation | Real-time for dynamic parts |
| Personalization | ❌ Same page for all users | ✅ User-specific content |
| Cache behavior | Entire page cached | Shell cached, dynamic fresh |
| Complexity | Moderate | Requires Suspense boundaries |
| Use case | Content that changes periodically | Content that's both static and personalized |
The Hybrid Reality
In practice, modern Next.js applications often combine strategies:
Homepage (Marketing) → SSG or ISR
Product Catalog → ISR (revalidate on changes)
Product Detail Page → PPR (static info + user cart)
User Dashboard → SSR or PPR (depends on content mix)
Checkout → SSR (fully personalized)
Blog Posts → SSG
Real-World Implementation Example
Let's build a product page that demonstrates PPR's power:
The Page Structure
// app/products/[id]/page.tsx
import { Suspense } from 'react'
import {
ProductHeader,
ProductImages,
ProductDescription,
ProductSpecs,
RelatedProducts
} from './static-components'
import {
UserPrice,
InventoryStatus,
AddToCart,
PersonalizedRecommendations,
RecentlyViewed
} from './dynamic-components'
import {
PriceSkeleton,
InventorySkeleton,
CartButtonSkeleton,
RecommendationsSkeleton
} from './skeletons'
export const experimental_ppr = true
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return (
<div className="product-page">
{/* STATIC SHELL - Instant delivery */}
<ProductHeader product={product} />
<ProductImages images={product.images} />
<div className="product-details">
<ProductDescription description={product.description} />
{/* DYNAMIC - User-specific pricing */}
<Suspense fallback={<PriceSkeleton />}>
<UserPrice productId={product.id} />
</Suspense>
{/* DYNAMIC - Real-time inventory */}
<Suspense fallback={<InventorySkeleton />}>
<InventoryStatus productId={product.id} />
</Suspense>
{/* DYNAMIC - Cart integration */}
<Suspense fallback={<CartButtonSkeleton />}>
<AddToCart product={product} />
</Suspense>
<ProductSpecs specs={product.specifications} />
</div>
{/* STATIC - Pre-rendered related products */}
<RelatedProducts categoryId={product.categoryId} />
{/* DYNAMIC - Personalized recommendations */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
{/* DYNAMIC - User's browsing history */}
<Suspense fallback={null}>
<RecentlyViewed />
</Suspense>
</div>
)
}
The User Experience
- 0-100ms: Static shell loads—product title, images, description visible
- 100-200ms: User can scroll, read description, view images
- 200-400ms: Price, inventory, cart button stream in
- 400-600ms: Personalized recommendations appear
- Page complete: Full interactive experience
Compare to traditional SSR where the user sees nothing until all data (including personalization) is fetched.
Best Practices for PPR
1. Design Good Fallbacks
Fallback UI is visible while dynamic content loads. Make it meaningful:
// ❌ Poor fallback
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
// ✅ Good fallback - maintains layout
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile />
</Suspense>
Skeleton components should:
- Match the dimensions of actual content
- Use appropriate colors (typically gray)
- Animate to indicate loading
- Prevent cumulative layout shift
2. Strategically Place Suspense Boundaries
Group related dynamic content:
// ❌ Too granular - many small loading states
<Suspense fallback={<NameSkeleton />}>
<UserName />
</Suspense>
<Suspense fallback={<AvatarSkeleton />}>
<UserAvatar />
</Suspense>
<Suspense fallback={<StatusSkeleton />}>
<UserStatus />
</Suspense>
// ✅ Grouped - coherent loading experience
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfo /> {/* Contains name, avatar, status */}
</Suspense>
3. Keep Static What Can Be Static
Maximize the static shell:
// ❌ Unnecessary dynamic boundary
<Suspense fallback={<TitleSkeleton />}>
<ProductTitle title={product.title} /> {/* This could be static! */}
</Suspense>
// ✅ Only dynamic where needed
<ProductTitle title={product.title} /> {/* Static */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice productId={product.id} /> {/* Actually needs user context */}
</Suspense>
4. Parallel Data Fetching
Dynamic components in separate Suspense boundaries fetch data in parallel:
// These fetch simultaneously, not sequentially
<Suspense fallback={<CartSkeleton />}>
<UserCart /> {/* Fetches cart data */}
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations /> {/* Fetches recommendation data */}
</Suspense>
5. Consider Mobile and Slow Connections
PPR particularly benefits mobile users:
- Static shell loads fast even on slow connections
- Users see content immediately
- Dynamic content streams as bandwidth allows
- Better perceived performance
Migration Strategy
From SSG to PPR
If you have static pages that need some personalization:
- Enable PPR on the route
- Identify dynamic elements
- Wrap them in Suspense with appropriate fallbacks
- Test that static portions remain cached
From SSR to PPR
If you have fully dynamic pages with static portions:
- Audit which data is actually user-specific
- Separate static and dynamic components
- Enable PPR and add Suspense boundaries
- Verify performance improvements
From ISR to PPR
If you use ISR but need real-time elements:
- Keep ISR for the static shell (revalidation still works)
- Add Suspense boundaries for real-time needs
- Dynamic content will always be fresh
- Static content revalidates per ISR settings
The Future: PPR as the Default
Vercel has stated their belief that PPR could become the default rendering model for web applications. Here's why:
The Unified Model
Instead of choosing between rendering strategies per-route, PPR allows:
- One mental model for all pages
- Automatic optimization based on code structure
- No explicit configuration of "static" vs "dynamic" routes
Framework Evolution
As React Server Components mature and streaming becomes more robust:
- Better tooling for identifying static/dynamic boundaries
- Automatic Suspense boundary suggestions
- More granular caching controls
- Broader CDN support for streaming
What This Means for Developers
- Simpler architecture decisions: Focus on component design, let the framework optimize
- Better defaults: Fast by default, dynamic when needed
- Progressive enhancement: Start static, add dynamism as needed
Current Limitations
PPR is still experimental. Current considerations:
Stability
- API may change before stable release
- Not recommended for production (as of Next.js 15)
- Monitor Next.js releases for updates
Infrastructure Requirements
- Full PPR support requires compatible hosting (Vercel has native support)
- Self-hosted deployments may need additional configuration
- CDN support for streaming responses varies
Complexity
- Requires understanding of React Suspense
- Fallback design adds development overhead
- Debugging streaming responses can be tricky
Conclusion: The Best of All Worlds
Partial Pre-Rendering represents a fundamental shift in how we think about web rendering. Instead of choosing between static speed and dynamic capabilities, PPR delivers both.
For users: Instant page loads with personalized, real-time content.
For developers: A unified model that simplifies architecture decisions.
For businesses: Better Core Web Vitals, improved SEO, and enhanced user experience without sacrificing functionality.
The era of "this page is either static or dynamic" is ending. The future is partial—and it's fast.
Frequently Asked Questions
Is PPR ready for production?
As of Next.js 15, PPR is experimental and not recommended for production applications. The API may change. Monitor Next.js releases and documentation for stability updates. Many teams are using it in development to prepare for when it's stable.
How does PPR affect SEO?
PPR is excellent for SEO. The static shell—including all important content, metadata, and structure—is immediately available to search engine crawlers. Dynamic content that streams in is typically user-specific and not critical for SEO anyway.
Does PPR work with the Pages Router?
No, PPR is designed for the App Router in Next.js. It relies on React Server Components and Suspense, which are App Router features. If you're using Pages Router, consider migrating to App Router to take advantage of PPR.
Can I use PPR without Vercel?
Yes, but with caveats. PPR can work on other hosting platforms, but the streaming and edge delivery features may require specific infrastructure. Vercel has the most complete PPR support through their Build Output API.
How do I debug PPR issues?
- Use React DevTools to inspect Suspense boundaries
- Check Network tab for streaming responses
- Monitor which components trigger dynamic rendering
- Use Next.js built-in debugging (set
DEBUG=pprenvironment variable)
Does PPR increase build times?
Minimally. PPR pre-renders static shells at build time (similar to SSG), so build times are comparable. The actual streaming happens at request time and doesn't affect builds.
How does PPR interact with caching?
The static shell is cached and served from the edge. Dynamic content is generated fresh for each request. You can still use ISR-style revalidation for the static shell while having truly dynamic Suspense boundaries.
What's the minimum React version for PPR?
PPR requires React 18+ for Suspense and streaming support. Next.js 15 includes the necessary React version.
Ready to Optimize Your Web Application Performance?
Partial Pre-Rendering represents the cutting edge of web performance optimization. Whether you're building a new application or looking to improve an existing one, understanding modern rendering strategies is essential.
Here's How DSRPT Can Help:
⚡ Performance Audit We'll analyze your current application's rendering strategy and Core Web Vitals, identifying opportunities for improvement with PPR and other optimizations.
🚀 Next.js Development Building a new application? We develop high-performance Next.js applications using the latest rendering strategies, including PPR-ready architectures.
📈 Migration Services Moving from an older framework or rendering approach? We handle migrations to modern Next.js with App Router, Server Components, and PPR preparation.
💬 Technical Consultation Not sure if PPR is right for your use case? We provide technical consultations to help you choose the optimal rendering strategy.
Why DSRPT?
We build web applications for businesses across Kuwait, the GCC, and Australia. As Google Premier Partners with deep expertise in Next.js and modern web development, we understand both the technology and its business impact.
Our approach:
- Performance-first: We obsess over Core Web Vitals and user experience
- Future-ready: We build with emerging best practices in mind
- Practical: We choose technologies that solve real problems, not just trendy ones
Your web application's performance directly impacts user experience, SEO, and conversion rates. Let's make sure you're using the best tools available.

