In modern web development, ensuring fast load times and a seamless user experience is crucial. Partial pre-rendering, a powerful feature in Next.js, bridges the gap between Static Site Generation (SSG) and Server-Side Rendering (SSR) by selectively prerendering parts of a page while leaving others dynamic. In this article, we’ll dive into partial pre-rendering, using a Cart Page as an example. (This is kind of a complex example because it implicates 2 dynamic sections. If you guys want a simpler one, let me know.)
What Is Partial Pre-Rendering?
Partial pre-rendering allows us to optimize performance by prerendering static sections of a page while dynamically fetching and rendering frequently changing or user-specific data. It uses features like:
- Static pre-rendering for predictable content (headers, footers, page layout).
- Dynamic rendering for data fetched at runtime (e.g., user carts, recommendations).
- Suspense boundaries to handle asynchronous loading elegantly.
Why Use Partial Pre-Rendering?
- Improved Performance: By prerendering static parts, users interact with pages faster.
- Better SEO: Static content is ready for search engine crawlers.
- Enhanced User Experience: Dynamic components load seamlessly within placeholders or fallbacks.
The Cart Page Example
Overview
On our Cart Page, the static and dynamic parts are as follows:
- Static: Page structure, layout components, and Fallbacks (Cart Content & Similar Products Placeholders).
- Dynamic:
- Cart Content: Fetched directly in the server component before rendering.
- Similar Products Section: Fetched dynamically after the page has loaded using a suspense boundary.
Code Walkthrough
Below is the code for the Cart Page using partial pre-rendering.
1. Static Layout and Dynamic Cart Content
The main structure of the page is prerendered, while cart data is fetched on the server using an asynchronous function in a server component.
// imports...
export default async function CartPage() {
// Fetch cart data on the server
const { cart, areProductsInCart, productsIds } = await initiateCart();
return (
<main>
<h1 className="sr-only">Cart Page</h1>
<Section className="max-w-[1400px]">
<CheckoutLayoutWrapper>
<CheckoutLayoutContent>
<SectionHeader
tag={areProductsInCart ? `${cart.itemsCount} item(s)` : undefined}
title="Cart Page"
isCheckoutPage
/>
<Card className="rounded-special w-full bg-primary-light shadow">
<CardContent>
{!areProductsInCart ? (
<EmptySection
title="Your cart is empty! Please add some products."
/>
) : (
<>
{/* Desktop Cart Content */}
<CartContentDesktop cartProducts={cart.cartProducts} />
{/* Mobile Cart Content */}
<CartContentMobile cartProducts={cart.cartProducts} />
</>
)}
</CardContent>
</Card>
</CheckoutLayoutContent>
{/* Sidebar for Cart Total and Coupon Code */}
{areProductsInCart && (
<CheckoutLayoutSidebar>
{/* Coupon Code */}
<CartCouponCode />
{/* Cart Total */}
<CartTotal cartData={cart?.allItemsWithData} />
</CheckoutLayoutSidebar>
)}
</CheckoutLayoutWrapper>
{/* Related Products Section */}
<CartSimilarProducts productIdsToExclude={productsIds ?? []} />
</Section>
</main>
);
}
2. Loading State
When cart data is being fetched, a loading.tsx component is displayed as a fallback. This is the default feature from NextJs App folder. This Loading component displays the placeholders of both the Cart Content And The Similar Products.
export default async function CartPage() {
return (
<main>
<h1 className="sr-only">Cart Page</h1>
<Section className="max-w-[1400px]">
<CheckoutLayoutWrapper>
<CheckoutLayoutContent>
<SectionHeaderLoader isCheckoutPage />
<Card className="rounded-special w-full bg-primary-light shadow">
<CardContent>
{/* Desktop */}
<CartContentDesktopLoader />
{/* Mobile */}
<CartContentMobileLoader />
</CardContent>
</Card>
</CheckoutLayoutContent>
{/* Sidebar */}
{/* Add Sidebar loader later maybe */}
</CheckoutLayoutWrapper>
{/* Related Products */}
<CartSimilarProductsLoader />
</Section>
</main>
);
}
3. Similar Products with Suspense
The Similar Products component fetches data dynamically and is wrapped in a suspense boundary. While data is being fetched, a placeholder (fallback) is shown.
export default function CartSimilarProducts({ productIdsToExclude }) {
return (
<Suspense fallback={<CartSimilarProductsLoader />}>
{/* Actual similar products component */}
<SimilarProducts productIdsToExclude={productIdsToExclude} />
</Suspense>
);
}
4. Flow Recap
- Step 1: Cart Content data is being fetched. On the website, the fallback (placeholder for both the cart content and the similar products) are pre-rendered.
- Step 2a: Cart Content data is fully fetched. On the website, all the pre-rendered content is replaced by the actual content.
- Step 2b: Similar Products Content is being fetched. The content is the same as above. However, in the actual content, there is the Cart content and a pre-rendered placeholder for the cart similar products. Though it is the same content and display as in the previous fallback it is not the exact same component.
- Step 3: Similar Products Content is fully fetched. On the website, we can now see both the Cart content and the Similar Products content.
Conclusion
Partial pre-rendering is a game-changer for building performant and dynamic applications. By leveraging Next.js's capabilities, we efficiently balance static and dynamic content on our Cart Page, enhancing both speed and user experience. If we are to be completely honest and literal, this is not Partial Pre-rendering, it is some kind of Nested Pre-Rendering. However, it does the trick if you are looking for Partial Pre-rendering. The Next.js team is actually working on developing a "real" Partial Pre-rendering feature, it is actually on "unstable" but might be operational in the next version.
Top comments (1)
Hello Nath 👋
Setting up a working project using next partial rendering is something I've been struggling with, so first thank you for this post 🙂 then is there anywhere you could deploy so I could see it live ? or maybe a github repo I could access ?
Thanks a lot, all the best, from France.
Guillaume