cd..blog

Optimizing Next.js 15 Partial Prerendering for High-Traffic E-commerce

const published = "Apr 25, 2026, 07:52 PM";const readTime = 5 min;
Next.jsReactWeb PerformanceTypeScriptVercel
Learn how to implement and optimize Partial Prerendering (PPR) in Next.js 15 to balance static performance with dynamic personalization in high-scale applications.

Optimizing Next.js 15 Partial Prerendering for High-Traffic E-commerce

As of April 2026, the stabilization of Partial Prerendering (PPR) in Next.js has fundamentally shifted how we approach the static-vs-dynamic tradeoff. For years, engineers were forced to choose between the SEO benefits and speed of Static Site Generation (SSG) or the personalization capabilities of Server-Side Rendering (SSR). PPR eliminates this binary choice by allowing a single route to host both static shells and dynamic islands without the overhead of client-side data fetching waterfalls.

This article explores the architectural implementation of PPR in a production e-commerce context, focusing on minimizing Time to First Byte (TTFB) while maintaining real-time inventory and user-specific pricing.

The Architecture of Partial Prerendering

PPR leverages React's Suspense boundaries to determine which parts of a page are static and which are dynamic. During the build process (or revalidation), Next.js generates a static HTML shell for the page. The dynamic holes are filled by streaming server-rendered content over the same HTTP connection once the data is available.

Why PPR Matters in 2026

In high-traffic e-commerce, every millisecond of latency correlates to conversion drop-off. Traditional SSR requires the server to fetch all data—product details, reviews, and user-specific discounts—before sending a single byte of HTML. With PPR, the product description and images (static) are served instantly from the Edge, while the 'Add to Cart' button and personalized pricing (dynamic) stream in as the database queries resolve.

Implementing PPR with Drizzle and PostgreSQL

To implement this effectively, your data fetching layer must be granular. Using Drizzle ORM, we can separate our queries to match our Suspense boundaries. Drizzle provides a TypeScript-first approach to SQL that ensures our dynamic components are type-safe from the database to the client.

Step 1: Enabling PPR

First, ensure your next.config.js is configured for the experimental feature (though nearing full stability in current canary builds):

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: 'incremental',
  },
};
export default nextConfig;

Setting ppr: 'incremental' allows you to opt-in specific routes rather than a global migration, which is critical for large-scale refactors.

Step 2: Structuring the Page

The key to PPR is the placement of Suspense. The code outside the boundary is static; the code inside is dynamic.

import { Suspense } from 'react';
import { ProductDetails, ProductSkeleton } from './components/product';
import { DynamicPricing } from './components/pricing';

export default function Page({ params }: { params: { id: string } }) {
  return (
    <main>
      {/* This part is static and cached at the Edge */}
      <ProductDetails id={params.id} />

      {/* This part is dynamic and streamed */}
      <Suspense fallback={<ProductSkeleton />}>
        <DynamicPricing id={params.id} />
      </Suspense>
    </main>
  );
}

Advanced Data Fetching Patterns

In a production environment, you cannot afford blocking calls. When using React Server Components (RSC), the execution order matters. If you await a dynamic call at the top level of your page, you accidentally opt the entire route into dynamic rendering, defeating the purpose of PPR.

Avoiding the 'Dynamic Contamination'

Dynamic contamination occurs when a dynamic function (like cookies(), headers(), or an un-cached fetch) is called in the static portion of the tree. To prevent this, isolate dynamic logic strictly within the components wrapped in Suspense.

// components/pricing.tsx
import { cookies } from 'next/headers';
import { db } from '@/db';

export async function DynamicPricing({ id }: { id: string }) {
  // Accessing cookies makes this component dynamic
  const cookieStore = await cookies();
  const userId = cookieStore.get('user_id')?.value;

  const price = await db.query.prices.findFirst({
    where: (prices, { eq }) => eq(prices.productId, id),
  });

  return <div>{price.amount}</div>;
}

Performance Tradeoffs and Monitoring

While PPR improves perceived performance, it introduces new complexities in monitoring. Traditional metrics like Largest Contentful Paint (LCP) might look excellent because the static shell loads instantly, but Cumulative Layout Shift (CLS) can suffer if your skeletons don't match the final rendered dimensions of the dynamic content.

Strategies for Layout Stability

  1. Explicit Dimensions: Always define height and width for skeletons. If a dynamic pricing block is 24px high, the skeleton must be exactly 24px.
  2. Streaming Priority: Use the connection API or priority hints if certain dynamic fragments are more critical than others.
  3. Edge Data Seeding: For global users, use a distributed database like Turso or Neon to keep the dynamic data fetch as close to the user as the static shell.

Handling State and Revalidation

With PPR, the static shell is cached until a revalidation event occurs. However, the dynamic portions are fetched on every request. This creates a hybrid caching model. You should use revalidateTag or revalidatePath to update the static shell when product metadata changes, while relying on standard database indexing for the dynamic pricing queries.

Example: On-Demand Revalidation

import { revalidateTag } from 'next/cache';

export async function updateProduct(formData: FormData) {
  'use server';
  // Update DB logic...
  
  // Purge the static shell cache
  revalidateTag('product-data');
}

Conclusion

Partial Prerendering in Next.js 15 represents the most significant advancement in web delivery since the introduction of Hydration. By strategically separating static content from dynamic user data using React Suspense, engineers can achieve sub-100ms TTFB for the shell while still delivering highly personalized experiences. The key to success lies in strict component isolation, precise skeleton design, and a deep understanding of how dynamic functions propagate through the component tree.