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

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

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.
Profiling and fixing a slow React render is easier when you stop guessing. Learn how to use React DevTools to find bottlenecks and optimize your app.
Read more