SSG vs SSR vs ISR: Choosing the Right Rendering Strategy in 2026

The short version: SSG is fastest but only works for content known at build time. SSR is flexible but costs server compute per request. ISR is the pragmatic middle ground — static performance with scheduled freshness. Partial Prerendering (Next.js 15 stable) is the new option for pages that are mostly static with a few dynamic sections. Choosing wrong costs you either performance or stale content.
Most rendering strategy debates miss the actual question: what is the data freshness requirement for this specific page? Start there, and the right rendering mode usually becomes obvious.
The four rendering modes explained
Static Site Generation (SSG)
Pages are built once at deploy time. The HTML is stored on a CDN and served to every user without any server involvement.
// app/blog/[slug]/page.tsx — Next.js 15 App Router
import { getBlogBySlug, getAllBlogSlugs } from '@/data/blogUtils';
// Pre-generate all blog slugs at build time
export async function generateStaticParams() {
const slugs = await getAllBlogSlugs();
return slugs.map(slug => ({ slug }));
}
// This page is fully static — built once, served from CDN
export default async function BlogPost({ params }: { params: { slug: string } }) {
const blog = await getBlogBySlug(params.slug);
return <article>{blog.content}</article>;
}
TTFB: 10–50ms (CDN cache hit) Content freshness: Only as fresh as the last build Infrastructure cost: Essentially zero per request Best for: Blog posts, documentation, marketing pages, anything where content doesn't change between deploys
Limitation: A 10,000-page site with daily content updates needs either very fast builds or ISR. Full static builds that take 20 minutes mean your published content is 20 minutes stale minimum.
Server-Side Rendering (SSR)
HTML is generated on the server for every request. The server fetches data, renders the page, and sends HTML to the browser.
// app/dashboard/orders/page.tsx — SSR (dynamic, per-request)
import { cookies } from 'next/headers';
import { getOrdersForUser } from '@/services/orders';
// No generateStaticParams = dynamic SSR by default in App Router
export default async function OrdersPage() {
const sessionCookie = cookies().get('session')?.value;
const userId = await validateSession(sessionCookie);
// Fetches fresh data on every request — this is the point of SSR
const orders = await getOrdersForUser(userId);
return (
<div>
{orders.map(order => <OrderCard key={order.id} order={order} />)}
</div>
);
}
TTFB: 100–500ms+ (server must fetch data and render before responding) Content freshness: Always current — fetches fresh data per request Infrastructure cost: Server compute per request; scales with traffic Best for: User-specific pages (dashboards, account pages), real-time data, anything requiring session/auth data per request
Limitation: Every request hits the server. Under high traffic, SSR requires horizontal scaling. Slow database queries directly delay the user's page load.
Incremental Static Regeneration (ISR)
Pages are pre-built like SSG but automatically revalidated after a set time window. The first request after the revalidation window triggers a background rebuild; the stale page is served to that user while the fresh version is generated.
// app/products/[id]/page.tsx — ISR with 1-hour revalidation
export const revalidate = 3600; // seconds
export async function generateStaticParams() {
const products = await getTopProducts(1000); // pre-build top 1000 products
return products.map(p => ({ id: p.id }));
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return <ProductDetail product={product} />;
}
TTFB: 10–50ms for cached pages (CDN hit); first request after revalidation window is served stale while fresh version rebuilds in background Content freshness: Within the revalidation window (1 hour in the example above) Infrastructure cost: Low — only one rebuild per revalidation window regardless of traffic volume Best for: Product pages, news articles, blog indexes, any content that changes on a predictable schedule and where N-minutes-stale is acceptable
On-demand revalidation — trigger a rebuild immediately when content changes, rather than waiting for the time window:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const { path, tag } = await request.json();
if (path) revalidatePath(path); // revalidate specific path
if (tag) revalidateTag(tag); // revalidate all pages tagged with this
return Response.json({ revalidated: true });
}
Call this endpoint from your CMS webhook when content is published. The CDN cache clears immediately.
Partial Prerendering (PPR) — Next.js 15 stable
PPR lets a single page have a static shell (served instantly from CDN) and dynamic sections (streamed in after the shell loads). No choosing between SSG and SSR for the whole route.
// app/product/[id]/page.tsx — PPR: static shell + dynamic sections
import { Suspense } from 'react';
import { ProductInfo } from '@/components/ProductInfo'; // static
import { StockStatus } from '@/components/StockStatus'; // dynamic — real-time
import { PersonalisedRecs } from '@/components/PersonalisedRecs'; // dynamic — user-specific
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id); // fetched at build time (static)
return (
<>
{/* Served from CDN instantly */}
<ProductInfo product={product} />
{/* Streamed in after shell loads — doesn't block initial render */}
<Suspense fallback={<StockSkeleton />}>
<StockStatus productId={params.id} />
</Suspense>
<Suspense fallback={<RecsSkeleton />}>
<PersonalisedRecs userId={getUserId()} />
</Suspense>
</>
);
}
Enable PPR in next.config.ts:
const nextConfig = {
experimental: {
ppr: true, // stable in Next.js 15
},
};
TTFB: 10–50ms for the static shell (CDN); dynamic sections stream in 100–500ms later Content freshness: Static sections as fresh as the build; dynamic sections always current Best for: E-commerce product pages, landing pages with personalisation, any page that's mostly static but has a few real-time or user-specific sections
Decision matrix
| Page type | Recommended mode | Why |
|---|---|---|
| Blog post, docs page | SSG | Content known at build time, no per-user data |
| Product listing (large catalogue) | ISR (1–24hr revalidation) | Mostly static, prices/stock update periodically |
| Product page with real-time stock | PPR | Static description + dynamic stock/recs |
| User dashboard | SSR | Always per-user, always current |
| Shopping cart | SSR | Session data required, must be current |
| Marketing landing page | SSG | Zero dynamic content, maximum CDN performance |
| News article | ISR (15–60 min) | Mostly static, occasional updates |
| Search results | SSR | Query-dependent, cannot pre-build |
| Admin panel | SSR | Auth-gated, user-specific, real-time |
Performance benchmarks by rendering mode
| Metric | SSG (CDN hit) | SSR (no cache) | ISR (cache hit) | PPR (shell) |
|---|---|---|---|---|
| TTFB | 10–50ms | 100–500ms | 10–50ms | 10–50ms |
| LCP target | Under 1.5s | 1.5–3s | Under 1.5s | Under 1.5s |
| Infrastructure cost | Near zero | Scales with traffic | Near zero | Near zero |
| Content freshness | Build time | Per request | Within revalidation window | Mixed |
| SEO suitability | Excellent | Excellent | Excellent | Excellent |
The mistake most teams make
Defaulting to SSR for everything because it's "safer" — it always has fresh data, so nothing can go wrong. This works until traffic scales. At 10,000 requests/minute, SSR for pages that haven't changed in months means you're paying server compute and adding latency to deliver HTML that could be served from CDN cache for a fraction of the cost.
My rule: start with SSG for everything. Add ISR where content updates on a schedule. Add SSR only for pages that genuinely need per-request, per-user data. Use PPR for the in-between cases.
For teams building on Next.js 15 specifically, the Next.js modern web development guide covers the App Router caching model in depth — understanding how fetch caching interacts with these rendering modes is necessary to avoid stale data bugs when mixing SSG and SSR on the same project.
Frequently Asked Questions

Written by
FNA Team
CEO & Founder at FNA Technology
Specializing in AI, automation, and scalable software solutions — helping businesses leverage cutting-edge technology to drive growth and innovation.
Work with us