The Rise of Partial Pre-Rendering: Next.js 15 and the Future of Web Performance

The Rise of Partial Pre-Rendering: Next.js 15 and the Future of Web Performance
The Rise of Partial Pre-Rendering: Next.js 15 and the Future of Web Performance
By: Abdulkader Safi
Software Engineer at DSRPT
16 min read

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:

  1. Static Shell: Header, navigation, footer, layout, product descriptions, marketing copy—anything that's the same for every user
  2. 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

ApproachWhen HTML is GeneratedWhat's Generated
SSGBuild timeEntire page (static)
SSREvery requestEntire page (dynamic)
ISRBuild time + revalidationEntire page (cached, refreshed)
PPRBuild time + request timeStatic 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:

  1. Next.js identifies which parts of your page are static and which require request-time data
  2. Static portions are rendered to HTML and cached
  3. Dynamic portions (wrapped in <Suspense>) are marked as "holes" with fallback content
  4. 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

AspectSSGPPR
Build time content✅ Everything✅ Static portions
Dynamic content❌ Not possible✅ Streamed
Personalization❌ None✅ In dynamic holes
Speed⚡ Fastest (fully cached)⚡ Nearly as fast
Use caseTruly static sitesDynamic sites wanting static speed

PPR vs SSR

AspectSSRPPR
Initial responseWaits for all dataInstant static shell
Time to first byteSlower (data dependent)Fast (static cached)
Server loadEvery request, full renderOnly dynamic portions
Edge deliveryLimitedStatic shell from edge
Use caseFully dynamic pagesMostly static with dynamic parts

PPR vs ISR

AspectISRPPR
Content freshnessPeriodic revalidationReal-time for dynamic parts
Personalization❌ Same page for all users✅ User-specific content
Cache behaviorEntire page cachedShell cached, dynamic fresh
ComplexityModerateRequires Suspense boundaries
Use caseContent that changes periodicallyContent 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

  1. 0-100ms: Static shell loads—product title, images, description visible
  2. 100-200ms: User can scroll, read description, view images
  3. 200-400ms: Price, inventory, cart button stream in
  4. 400-600ms: Personalized recommendations appear
  5. 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:

  1. Enable PPR on the route
  2. Identify dynamic elements
  3. Wrap them in Suspense with appropriate fallbacks
  4. Test that static portions remain cached

From SSR to PPR

If you have fully dynamic pages with static portions:

  1. Audit which data is actually user-specific
  2. Separate static and dynamic components
  3. Enable PPR and add Suspense boundaries
  4. Verify performance improvements

From ISR to PPR

If you use ISR but need real-time elements:

  1. Keep ISR for the static shell (revalidation still works)
  2. Add Suspense boundaries for real-time needs
  3. Dynamic content will always be fresh
  4. 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=ppr environment 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.

Request a Performance Audit →

🚀 Next.js Development Building a new application? We develop high-performance Next.js applications using the latest rendering strategies, including PPR-ready architectures.

Discuss Your Project →

📈 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.

Plan Your Migration →

💬 Technical Consultation Not sure if PPR is right for your use case? We provide technical consultations to help you choose the optimal rendering strategy.

Book a Consultation →


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.

Start the Conversation →

Subscribe to our Newsletter!
Copyrights © 2025 DSRPT | All Rights Reserved