Next.js Rendering Strategies: SSR, SSG, ISR, and PPR Explained
A complete guide to Next.js rendering strategies—SSR, SSG, ISR, and Partial Prerendering—with practical advice on when to use each approach.
Tags
Next.js Rendering Strategies: SSR, SSG, ISR, and PPR Explained
Next.js provides four distinct rendering strategies—Static Site Generation (SSG), Server-Side Rendering (SSR), Incremental Static Regeneration (ISR), and Partial Prerendering (PPR)—each designed for different trade-offs between performance, data freshness, and personalization. The right choice depends on how often your data changes, whether content is personalized, and how critical time-to-first-byte is for your users. This guide explains each strategy with practical examples and a clear framework for choosing between them.
TL;DR
SSG generates pages at build time for maximum speed. SSR generates pages on every request for maximum freshness. ISR blends both by serving static pages and revalidating in the background. PPR takes it further by splitting a single page into static and dynamic parts, streaming the dynamic content after the static shell loads instantly. You can mix all four strategies per route in a single Next.js application.
Why This Matters
Rendering strategy directly impacts three metrics your users and business care about: page load speed (affects bounce rates and conversions), data freshness (affects user trust and accuracy), and infrastructure cost (affects your hosting bill). Choosing SSR for a page that never changes wastes server resources. Choosing SSG for a dashboard with real-time data shows stale information. Understanding the trade-offs lets you optimize for what matters most on each route.
How It Works
Static Site Generation (SSG)
SSG generates HTML at build time. The resulting pages are deployed to a CDN and served as static files, giving the fastest possible time-to-first-byte since no server-side computation happens at request time.
In the App Router, pages are statically generated by default when they do not use dynamic functions:
// app/about/page.tsx
// This page is automatically statically generated
export default function AboutPage() {
return (
<main>
<h1>About Us</h1>
<p>Our company was founded in 2020...</p>
</main>
);
}For pages that fetch data at build time:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}Best for: Marketing pages, documentation, blog posts, landing pages—any content that does not change between requests.
Server-Side Rendering (SSR)
SSR generates HTML on every request. The server fetches data, renders the component tree to HTML, and sends the complete page to the browser. This ensures data is always current but adds server processing time to every request.
In the App Router, you opt into SSR by using dynamic functions or by explicitly setting the route as dynamic:
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const cookieStore = cookies();
const sessionToken = cookieStore.get('session')?.value;
const userData = await fetchUserData(sessionToken);
const analytics = await fetchAnalytics(userData.id);
return (
<main>
<h1>Welcome back, {userData.name}</h1>
<AnalyticsGrid data={analytics} />
<RecentActivity userId={userData.id} />
</main>
);
}Using cookies(), headers(), or searchParams automatically makes a route dynamically rendered. You can also force it with export const dynamic = 'force-dynamic'.
Best for: Personalized dashboards, user-specific content, pages that depend on request headers or cookies, real-time data displays.
Incremental Static Regeneration (ISR)
ISR serves statically generated pages while revalidating them in the background. You define a revalidation interval, and after that period expires, the next request triggers a background regeneration. The stale page is served immediately while the new version is generated.
// app/products/[id]/page.tsx
// Time-based revalidation
export const revalidate = 3600; // Revalidate every hour
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`)
.then((res) => res.json());
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>${product.price}</span>
</main>
);
}You can also trigger revalidation on demand using revalidatePath or revalidateTag:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { path, tag } = await request.json();
if (tag) {
revalidateTag(tag);
} else if (path) {
revalidatePath(path);
}
return Response.json({ revalidated: true, now: Date.now() });
}
// In your data fetching, tag the cache
const product = await fetch(`https://api.example.com/products/${id}`, {
next: { tags: [`product-${id}`] },
});Best for: E-commerce product pages, blog posts with occasional updates, content that changes periodically but does not need to be real-time.
Partial Prerendering (PPR)
PPR is the newest rendering strategy in Next.js. It combines static and dynamic rendering within a single route. The static portions of the page are served instantly from the edge, while dynamic portions are streamed in using React Suspense boundaries.
// app/page.tsx
import { Suspense } from 'react';
// Enable PPR for this route
export const experimental_ppr = true;
export default function HomePage() {
return (
<main>
{/* Static — served instantly from CDN */}
<Header />
<HeroBanner />
{/* Dynamic — streamed after initial load */}
<Suspense fallback={<ProductGridSkeleton />}>
<PersonalizedProducts />
</Suspense>
{/* Static */}
<FeaturesSection />
{/* Dynamic */}
<Suspense fallback={<CartSummarySkeleton />}>
<CartSummary />
</Suspense>
{/* Static */}
<Footer />
</main>
);
}
// This component is dynamic because it reads cookies
async function PersonalizedProducts() {
const cookieStore = cookies();
const userId = cookieStore.get('userId')?.value;
const products = await getRecommendations(userId);
return <ProductGrid products={products} />;
}With PPR, the static shell (Header, HeroBanner, FeaturesSection, Footer) is served from the edge in milliseconds. The dynamic components (PersonalizedProducts, CartSummary) stream in as they resolve, with skeleton fallbacks shown in the meantime.
Best for: Pages that mix static content with personalized or frequently changing sections—e-commerce homepages, news feeds with personalized rankings, dashboards with a static layout and dynamic data.
Comparison Table
| Strategy | Generation Time | Data Freshness | TTFB | Personalization | Infrastructure Cost |
|---|---|---|---|---|---|
| SSG | Build time | Stale until rebuild | Fastest | None | Lowest (CDN only) |
| SSR | Every request | Always fresh | Slowest | Full | Highest (server per request) |
| ISR | Build + background | Configurable staleness | Fast | None (per-path) | Low-Medium |
| PPR | Static shell at build, dynamic at request | Mixed | Fast shell, streaming dynamic | Partial (per-component) | Medium |
Practical Implementation
A real-world Next.js application typically uses multiple strategies. Here is how you might structure an e-commerce site:
// app/layout.tsx — Static shell for the entire app
export default function RootLayout({ children }) {
return (
<html>
<body>
<Navigation /> {/* Static */}
{children}
<Footer /> {/* Static */}
</body>
</html>
);
}
// app/page.tsx — PPR for homepage
export const experimental_ppr = true;
// app/products/page.tsx — ISR for product listing
export const revalidate = 300; // 5 minutes
// app/products/[slug]/page.tsx — ISR with on-demand revalidation
export const revalidate = 3600; // 1 hour, plus webhook-triggered revalidation
// app/dashboard/page.tsx — SSR for user dashboard
export const dynamic = 'force-dynamic';
// app/about/page.tsx — SSG (default, no dynamic functions)Common Pitfalls
Defaulting everything to SSR. Developers coming from traditional server frameworks often make every page dynamic out of habit. This wastes server resources and increases TTFB for pages that could be statically generated.
Setting revalidation intervals too low. Setting revalidate = 1 on ISR pages effectively turns them into SSR pages with extra caching complexity. If you need data that fresh, use SSR directly.
Forgetting that dynamic functions opt out of static generation. Calling cookies(), headers(), or accessing searchParams in a Server Component automatically makes the entire route dynamic. This is often unintentional—extract dynamic portions into Suspense-wrapped child components to preserve static generation for the rest of the page.
Not providing Suspense fallbacks. Without Suspense boundaries around dynamic components in PPR, the entire page becomes dynamic. The Suspense boundary is what tells Next.js where to split static and dynamic content.
Ignoring build times for large static sites. SSG with generateStaticParams generates every page at build time. For sites with thousands of pages, this can make builds take minutes. Use ISR for long-tail content and only statically generate your most-visited pages.
When to Use (and When Not To)
Use SSG when: content is the same for all users and changes only at deploy time. Examples: documentation, marketing pages, blog posts without live comments.
Use SSR when: every request needs fresh, personalized data. Examples: authenticated dashboards, search results pages, checkout flows.
Use ISR when: content updates periodically but does not need to be real-time. Examples: product pages, news articles, content management system pages.
Use PPR when: a single page mixes static layout with personalized or dynamic sections. Examples: e-commerce homepages, social feeds, any page where the "chrome" is static but the "content" is personalized.
Avoid over-engineering: If your entire site is a blog with content that changes at deploy time, SSG is all you need. Do not add ISR or PPR complexity for content that a simple next build handles perfectly.
FAQ
What is the difference between SSR and SSG in Next.js?
SSG generates HTML at build time and serves it from a CDN for the fastest possible response. SSR generates HTML on every request, ensuring data is always current but adding server processing time to each request.
What is ISR in Next.js and how does it work?
ISR serves statically generated pages while revalidating them in the background. After a configurable time interval, the next request triggers background regeneration. The stale page is served immediately while the fresh version is built for subsequent requests.
What is Partial Prerendering in Next.js?
PPR splits a single route into static and dynamic parts. The static shell is served instantly from the edge, while dynamic portions stream in via React Suspense boundaries, combining the speed of SSG with the freshness of SSR.
When should I use SSR instead of SSG?
Use SSR when page content depends on the incoming request—user authentication, cookies, query parameters—or when data changes too frequently for any form of static generation to be practical.
Can I mix rendering strategies in a Next.js application?
Yes. Next.js allows per-route rendering decisions. Marketing pages can use SSG, dashboards can use SSR, blogs can use ISR, and individual routes can use PPR—all within the same application.
Collaboration
Need help with a project?
Let's Build It
I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.
Related Articles
How to Design API Contracts Between Micro-Frontends and BFFs
Learn how to design stable API contracts between Micro-Frontends and Backend-for-Frontend layers with versioning, ownership boundaries, error handling, and schema governance.
Next.js BFF Architecture
An architectural deep dive into using Next.js as a Backend-for-Frontend, including route handlers, server components, auth boundaries, caching, and service orchestration.
Next.js Cache Components and PPR in Real Apps
A practical guide to using Next.js Cache Components and Partial Prerendering in real applications, with tradeoffs, cache strategy, and freshness considerations.