React 19.2's Partial Pre-Rendering: Making Your Apps Load 10x Faster

React 19.2's Partial Pre-Rendering: Making Your Apps Load 10x Faster

Welcome Back!

Quick series recap for those following along:

  • Day 1: Activity Component (preserve state like pausing a TV)
  • Day 2: useEffectEvent Hook (clean up dependency hell)
  • Day 3 (today): Partial Pre-Rendering (lightning-fast page loads)

Apologies for the small break in continuity—life as a Senior Software Engineer keeps me busy with exciting projects! But I'm back with another deep-dive, and I promise to deliver the full series. Today's topic is one of React 19.2's most revolutionary features.

Let's dive into Partial Pre-Rendering (PPR)—the technique that's transforming how modern web apps load.

The Problem: Users Wait While Servers Work

Imagine this scenario: You're building an e-commerce product page. Your user clicks "View Product." They want to see:

✅ Product title

✅ Images

✅ Description

✅ Price

But your backend also needs to:

🔄 Fetch 300 user reviews

🔄 Generate personalised recommendations

🔄 Check real-time inventory

🔄 Load promotional banners

The old reality: Everything blocks. Users see a blank screen for 3-5 seconds while the server waits for ALL data to arrive.

┌─────────────────────────────────────┐
│           LOADING... (5 seconds)    │
│                                     │
│        [Blank Screen]               │
│                                     │
│   [User gets frustrated]            │
│                                     │
│   [User clicks back]                │
└─────────────────────────────────────┘        

The human cost: 53% of mobile users abandon sites that take over 3 seconds to load. Your beautifully designed product page never gets seen.

The Solution: Show Something Immediately

Partial Pre-Rendering (PPR) flips this logic: Show users what you CAN show immediately, then fill in the rest.

The restaurant analogy:

  • Old way: Wait for the entire meal to cook before serving anything
  • PPR way: Serve the bread and salad immediately, then bring the main course as it finishes

User clicks "View Product"
    ↓
CDN serves pre-built static shell (0.3 seconds)
    ├─ Product title
    ├─ Images
    ├─ Description  
    ├─ [Reviews skeleton]
    └─ [Recommendations skeleton]
    ↓
USER SEES CONTENT IMMEDIATELY ✅
    ↓
Server fetches reviews (background) (1.2s)
    ↓
Server generates recommendations (background) (1.0s)
    ↓
Content streams in gradually
    ↓
Page fully loaded (1.5 seconds total)

Result: User sees product in 0.3s instead of 5s        

How PPR Actually Works

The Two-Phase Approach

PPR splits your rendering into two distinct phases:

Phase 1: Build Time (Pre-Render Static Shell)

// At deployment (happens once)
export default function ProductPage({ productId }) {
  // React identifies what's static vs dynamic
  return (
    <div>
      {/* These pre-render at build time */}
      <Header />
      <ProductTitle productId={productId} />  // Static data
      <ProductImages productId={productId} /> // Static data
      <ProductDescription productId={productId} /> // Static data
      
      {/* These stream at runtime */}
      <Suspense fallback={<ReviewsLoading />}>
        <ProductReviews productId={productId} />  // Dynamic data
      </Suspense>
      
      <Suspense fallback={<RecommendationsLoading />}>
        <Recommendations userId={userId} />  // Dynamic data
      </Suspense>
      
      <Footer />
    </div>
  );
}        

What happens at build time:

  • React pre-renders everything except <Suspense> boundaries
  • Creates a static HTML shell with placeholders for dynamic content
  • Uploads to CDN for instant delivery

Phase 2: Request Time (Stream Dynamic Content)

User visits page
    ↓
CDN delivers static shell (0.3s) ← User sees content
    ├─ Product title ✅
    ├─ Images ✅
    ├─ Description ✅
    ├─ [Reviews loading...] ⏳
    └─ [Recommendations loading...] ⏳
    ↓
Server fetches dynamic data (parallel)
    ↓
Reviews arrive (0.8s) → Fills placeholder
    ↓
Recommendations arrive (1.2s) → Fills placeholder
    ↓
Page fully interactive (1.5s total)        

The Suspense Magic

The <Suspense> component is your marker for "this content needs data":

// Dynamic content with fallback
<Suspense fallback={<LoadingSkeleton />}>
  <ProductReviews productId={productId} />
</Suspense>

// Or more sophisticated
<Suspense fallback={
  <div className="reviews-loading">
    <div className="review-card skeleton"></div>
    <div className="review-card skeleton"></div>
    <div className="review-card skeleton"></div>
  </div>
}>
  <ProductReviews productId={productId} />
</Suspense>        

What Suspense does:

  1. Blocks rendering of its content until data is ready
  2. Shows fallback during loading
  3. Streams content when data arrives
  4. Handles errors gracefully

Real-World Architecture: E-Commerce Example

Before PPR: The Blocking Flow

// Old way - everything blocks
async function getProductPage(productId) {
  // Wait for product data
  const product = await fetchProduct(productId); // 200ms
  
  // Wait for reviews
  const reviews = await fetchReviews(productId); // 1200ms
  
  // Wait for recommendations
  const recs = await generateRecommendations(userId); // 1000ms
  
  // Wait for pricing
  const pricing = await getDynamicPricing(productId); // 800ms
  
  // Render everything together
  return renderProductPage(product, reviews, recs, pricing);
  // Total: 3,200ms (3.2 seconds)
}

// User sees blank screen for 3.2 seconds        

After PPR: Parallel Processing


// New way - static first, dynamic parallel
export default function ProductPage({ productId }) {
  return (
    <div>
      {/* Static content - renders immediately */}
      <StaticProductInfo productId={productId} />
      
      {/* Dynamic content loads in parallel */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={productId} />
      </Suspense>
      
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations userId={userId} />
      </Suspense>
      
      <Suspense fallback={<PricingSkeleton />}>
        <DynamicPricing productId={productId} />
      </Suspense>
    </div>
  );
}        

What happens behind the scenes:

  1. Build time: StaticProductInfo pre-renders to HTML → CDN
  2. Request time: CDN serves static content (0.3s)
  3. Parallel: Three dynamic requests happen simultaneously
  4. Streaming: Each arrives and fills its placeholder

Timeline:
0.3s: Static content ✅
0.8s: Reviews arrive ✅  
1.2s: Recommendations arrive ✅
1.5s: Pricing arrives ✅
1.5s: Fully interactive ✅        

Result: 5x faster perceived load time.

Performance Numbers: Before vs After

Core Web Vitals Improvement

Article content


Setting Up PPR: Step-by-Step

Step 1: Enable PPR in Your Framework

Next.js (Recommended):

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: 'incremental',  // Enable Partial Pre-Rendering
    missingSuspenseWithCSRBailout: 'swr'
  }
};

module.exports = nextConfig;        

Standalone React :

// App config
import { createRoot } from 'react-dom/client';
import { hydrateRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App pprEnabled={true} />);        

Step 2: Identify Your Static/Dynamic Boundaries

Audit your pages:

Static Content (Pre-render these):

  • Headers and footers
  • Product descriptions
  • Article content
  • Static images
  • Navigation
  • Layout structure

Dynamic Content (Suspense these):

  • User-specific data
  • Real-time metrics
  • Database queries
  • API calls
  • User-generated content
  • Personalization

Step 3: Add Suspense Boundaries

// ❌ Bad: Everything in one block
function Dashboard() {
  const [metrics, setMetrics] = useState([]);
  const [chartData, setChartData] = useState([]);
  
  return (
    <div>
      <Metrics metrics={metrics} />
      <SalesChart data={chartData} />
    </div>
  );
}

// ✅ Good: Separate boundaries
function Dashboard() {
  return (
    <div>
      {/* Static layout */}
      <Header />
      <Sidebar />
      
      {/* Dynamic zones */}
      <Suspense fallback={<MetricsSkeleton />}>
        <Metrics />
      </Suspense>
      
      <Suspense fallback={<ChartSkeleton />}>
        <SalesChart />
      </Suspense>
      
      <Footer />
    </div>
  );
}        

Step 4: Create Meaningful Loading States

Don't just use spinners—create realistic skeletons:

function ReviewsSkeleton() {
  return (
    <div className="reviews-container">
      {[...Array(3)].map((_, i) => (
        <div key={i} className="review-skeleton">
          <div className="skeleton-avatar"></div>
          <div className="skeleton-text">
            <div className="skeleton-line"></div>
            <div className="skeleton-line short"></div>
          </div>
          <div className="skeleton-rating"></div>
        </div>
      ))}
    </div>
  );
}

function RecommendationsSkeleton() {
  return (
    <div className="recommendations-grid">
      {[...Array(4)].map((_, i) => (
        <div key={i} className="product-card-skeleton">
          <div className="skeleton-image"></div>
          <div className="skeleton-title"></div>
          <div className="skeleton-price"></div>
        </div>
      ))}
    </div>
  );
}        

The Bottom Line

Partial Pre-Rendering isn't just a performance optimization—it's a fundamental improvement in user experience.

Instead of making users wait for your server to do everything, you give them immediate value while handling complexity in the background.

The transformation:

  • User experience: From frustrating blank screens to instant content
  • Performance: From 3-5 second loads to 0.3-1.5 second interactions
  • Business impact: From 40% bounce rates to 20-25% conversion increases
  • Technical architecture: From blocking waterfalls to parallel streams

Implementation simplicity:

  • Add Suspense boundaries (React handles the rest)
  • Create meaningful loading states
  • Monitor Core Web Vitals
  • Gradually roll out across your app

If you're using React 19.2, PPR should be your first performance initiative. The ROI is immediate and substantial.

For your next project: Identify your highest-traffic pages, add Suspense boundaries, and watch your engagement metrics transform.


To view or add a comment, sign in

More articles by Suraj Raj

Others also viewed

Explore content categories