Next.js Partial Prerendering: Optimizing Dynamic E-commerce Feeds
Next.js Partial Prerendering (PPR) lets you mix static and dynamic UI in one route. Learn how to optimize your e-commerce product feeds for instant loading.

Last month, our e-commerce dashboard was hitting a wall. We were relying on heavy client-side fetching for personalized product recommendations, leading to a "layout shift" nightmare and a sluggish Time to First Byte (TTFB). Moving to Streaming and Suspense in Next.js: Optimize Your Page Load helped, but we still needed a way to serve the static shell instantly while keeping our dynamic inventory data fresh. That’s where Next.js Partial Prerendering (PPR) became our primary tool for performance optimization.
Why Partial Prerendering matters for e-commerce
In a typical product feed, the layout, header, and static product descriptions don’t change often. However, the price, stock status, and "add to cart" buttons depend on real-time database queries.
Before we adopted PPR, we had two bad choices:
- Static Generation (SSG): Fast, but prices would be stale until the next revalidation cycle.
- Server-Side Rendering (SSR): Always fresh, but the user waited for the database query on every single request, killing our TTFB.
With PPR, we get the best of both worlds. We render the static shell at build time and stream the dynamic components as they resolve.
Configuring the App Router for PPR
To get started, ensure you're on next@15.0.0 or higher. PPR is still an experimental feature in many contexts, so we need to enable it in next.config.js:
JAVASCRIPT/** @type {import(CE9178">'next').NextConfig} */ const nextConfig = { experimental: { ppr: CE9178">'incremental', }, }; module.exports = nextConfig;
Using incremental is safer than true because it lets you opt-in on a per-page basis using the experimental_ppr segment config. This is crucial because you don’t want to break third-party libraries that might rely on specific headers or cookies you aren't ready to handle in a static context.
Implementing the dynamic feed

The magic happens when you combine React Server Components with Suspense. When you wrap a component in Suspense, Next.js treats everything inside the fallback as a dynamic hole.
Here is how we structured our product feed:
TSX// app/products/page.tsx import { Suspense } from CE9178">'react'; import ProductList from CE9178">'@/components/ProductList'; import Skeleton from CE9178">'@/components/Skeleton'; export const experimental_ppr = true; export default function ProductPage() { return ( <main> <h1>Our Latest Collection</h1> <Suspense fallback={<Skeleton />}> <ProductList /> </Suspense> </main> ); }
In this setup, the <h1> and the layout are sent to the browser immediately as static HTML. The ProductList component, which performs a database fetch, is deferred. When the data is ready, Next.js streams the result into the existing DOM.
Lessons from the trenches
We initially tried to wrap the entire page in one giant Suspense boundary. That was a mistake. We saw the header flicker—which is the exact opposite of what we wanted.
When you implement Partial Prerendering, keep your boundaries tight. Only wrap the specific components that require dynamic data. If you wrap too much, you lose the benefits of the static shell.
Also, remember that Caching and revalidation in the Next.js App Router: A Practical Guide still applies. PPR doesn't mean you ignore your data cache. If your product price changes, you still need to trigger a revalidation, or your "dynamic" component will just serve the cached version from the last successful stream.
Performance impact

After shipping this, we saw our TTFB drop by roughly 300ms on mobile devices. It’s not just about the numbers; the perceived performance is significantly better. Users see the page structure immediately, and the products "pop" into place within a few hundred milliseconds.
One caveat: debugging streaming can be tricky. If a component fails to stream, check your server logs for serialization errors. Since we are using React Server Components, ensure your data fetching logic is clean and doesn't leak sensitive tokens to the client.
I'm still experimenting with how PPR interacts with complex authentication flows. For now, we're keeping sensitive user-specific data outside of the PPR-enabled routes, but I’m optimistic that as the App Router ecosystem matures, we'll be able to push even more logic to the edge.
If you're struggling with slow product pages, stop trying to optimize the entire request. Start by carving out the static parts, and let the dynamic content stream in exactly when it's ready.