跳到主要内容
Zomer Gregorio

Zomer Gregorio

软件工程师

简历
前端
  • TypeScript
  • React
  • Next.js
  • TanStack
  • Tailwind CSS
后端
  • Node.js
  • Hono
  • Express
  • Django
数据
  • PostgreSQL
  • Redis
  • Drizzle
  • Prisma
基础设施/工具
  • Docker
  • GitHub Actions
  • Cloudflare
  • Vercel
博客

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

· Next.js · React · Web Performance · TypeScript · Vercel

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.