The End of the SSR vs. SSG Debate: Why Partial Prerendering is the New Standard
For the better part of a decade, web developers have been trapped in a binary choice. If you wanted SEO and lightning-fast initial page loads, you chose Static Site Generation (SSG). If you needed dynamic, personalized data—like a user dashboard, a real-time analytics feed, or an e-commerce cart—you were forced into Server-Side Rendering (SSR).
This trade-off was the "original sin" of modern web architecture. You either sacrificed interactivity for speed, or sacrificed speed for dynamic capabilities.
In 2026, with the stabilization of Partial Prerendering (PPR) in Next.js 16, that debate is officially over. We no longer have to choose.
What is Partial Prerendering?
Partial Prerendering (PPR) is a rendering strategy that allows a single route to combine static and dynamic content in one HTTP response.
At build time, Next.js generates a static HTML shell for your route. This shell includes all the content that can be determined ahead of time—navigation, layouts, hero sections, and static data. Crucially, it leaves "holes" (Suspense boundaries) where dynamic, request-specific data will eventually go.
When a user requests the page, the CDN serves the static shell instantly from the edge. Meanwhile, the server continues rendering the dynamic portions of the page and streams them into those holes the moment they are ready.
The Engineering Shift: Static Shells vs. Dynamic Holes
The magic of PPR lies in how it handles the "boundary." In previous versions of Next.js, accessing a request-time API like cookies(), headers(), or searchParams would force the entire page to be rendered dynamically on the server.
With PPR, that dynamic-ness is contained. If you wrap a component that accesses cookies() in a Suspense boundary, only that component—and its subtree—becomes dynamic. The rest of the page remains part of the static, cacheable shell.
A Practical Example
Consider a dashboard page. You want the navigation and layout to load instantly, but the "User Profile" and "Recent Notifications" depend on the current user's session.
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { UserProfile } from '@/components/user-profile';
import { Notifications } from '@/components/notifications';
export default function DashboardPage() {
return (
<main>
<h1>My Dashboard</h1>
{/* Static content prerenders into the shell */}
<nav>...</nav>
{/* Dynamic holes streamed in at request time */}
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<NotificationsSkeleton />}>
<Notifications />
</Suspense>
</main>
);
}
In this setup, UserProfile and Notifications are the dynamic holes. Because they are wrapped in Suspense, the static shell (the <h1>, the <nav>, and the skeleton loaders) is shipped to the user immediately. The server then resolves the user data and streams the real components into the page without the user ever needing to perform a client-side fetch.
Why This is the New Standard
The performance benefits are measurable and significant. In a recent high-traffic dashboard project, we saw a 40% improvement in responsiveness.
- Lower TTFB: Because the static shell is served from the CDN edge, your Time to First Byte (TTFB) drops to near-zero, regardless of how slow your database or backend API might be.
- No Layout Shift: By using consistent skeleton fallbacks, you can ensure that the page structure is defined instantly, preventing Cumulative Layout Shift (CLS) as the dynamic content streams in.
- Better SEO: Search engines receive the full HTML structure, including the static content and the skeleton placeholders, ensuring your content is indexed without waiting for client-side JavaScript execution.
- Simplified Architecture: You no longer need to maintain complex client-side data fetching logic just to make a page feel fast. The framework handles the orchestration of static and dynamic content seamlessly.
How to Get Started
PPR is now the default rendering model in Next.js 16 (enabled via cacheComponents: true in your next.config.ts).
To adopt it successfully:
- Identify your boundaries: Look for components that use
cookies(),headers(), orsearchParams. - Wrap dynamic components: Use
Suspenseto contain these dynamic parts. - Use 'use cache': For data that is expensive to fetch but doesn't change on every request, use the
'use cache'directive to include it in your static shell.
The era of choosing between speed and dynamic capabilities is behind us. By embracing Partial Prerendering, you aren't just optimizing your app; you're adopting the new standard for modern web performance.
How are you balancing static speed with dynamic requirements in your current stack? The shift to PPR is a journey, and the payoff is a faster, more resilient user experience.
Top comments (0)