Skip to main content

SSR vs SSG vs ISR vs PPR: Rendering 2026

·PkgPulse Team

Choosing a rendering strategy is one of the highest-leverage decisions in web development. A wrong choice means either slow pages or stale data — and fixing it later means rearchitecting routes, caching layers, and deployment pipelines. In 2026, the standard strategies are SSG, SSR, ISR, and the newcomer PPR — each solving a different problem in the static/dynamic spectrum.

This guide cuts through the theory. Here's when to use each, what the real performance differences are, and how they map to real-world use cases.

TL;DR

Use SSG for content that doesn't change per-request (docs, blogs, marketing). Use SSR for fully dynamic, personalized pages where every byte must be fresh. Use ISR for content that's shared across users but changes periodically (news, product catalogs). Use PPR for pages that mix static layout with dynamic per-user content — it's the strategy that finally closes the gap between static performance and server-side freshness. PPR is currently experimental in Next.js (canary builds in v15, evolved into Cache Components in v16) and is Next.js-only — default to ISR + on-demand revalidation for most teams today.

Key Takeaways

  • TTFB: SSG ~50ms → ISR ~50ms → PPR ~50ms (shell) → SSR ~200-500ms
  • SSG generates HTML at build time — fastest possible delivery, but content is frozen until rebuild
  • ISR adds revalidate to SSG — content refreshes in the background on a schedule without a full rebuild
  • SSR renders on every request — freshest data, highest server load, no CDN full-page caching
  • PPR serves a static shell from CDN + streams dynamic holes — best of SSG performance with SSR freshness
  • CSR (client-side rendering) is often the worst of all worlds for SEO and performance — avoid it for content pages
  • Framework support: Next.js supports all four; Astro supports SSG/SSR/ISR; SvelteKit supports SSG/SSR; Remix is SSR-first

At a Glance

StrategyTTFBData FreshnessPersonalizationCDN CacheableSEOBest For
SSG20–50msBuild-time✅ Full pageDocs, blogs, marketing
ISR20–50msRevalidate window✅ Full pageNews, product catalogs
SSR (serverless, warm)80–200msEvery requestDashboards, auth flows
SSR (cold start)300–900msEvery requestAvoid unmitigated
PPR (shell)20–50msEvery request (dynamic holes)✅ Shell onlyMixed static/dynamic
CSR20–50ms (empty HTML)Client fetch (~1-2s)⚠️ PoorAdmin tools, non-SEO apps

Static Site Generation (SSG)

SSG renders all pages to static HTML at build time. The output — plain HTML, CSS, and JavaScript files — is deployed to a CDN. There is no server involved at request time. The user gets raw HTML served from the nearest CDN node.

How It Works

Build time:
  getStaticProps() → fetch data → render React → emit HTML file

Request time:
  User → CDN edge → serve static HTML
  (No server, no database, no runtime compute)

When SSG Is the Right Choice

  • Documentation sites — content changes when you push, not per-user
  • Marketing pages — the same landing page serves every visitor
  • Blogs and editorial content — articles change infrequently
  • Portfolio sites — nearly static by definition

The SSG Limit: Stale Data and Build Times

SSG breaks down when data changes frequently or at scale. A 10,000-page documentation site might take 8 minutes to rebuild just to update a single page. A product catalog with hourly price changes can't afford a full rebuild every hour. That's what ISR solves.

// Next.js SSG — data fetched at build time only
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 getPost(params.slug)
  return <Article post={post} />
}
// No revalidate = pure SSG, never re-renders after build

Incremental Static Regeneration (ISR)

ISR extends SSG with a revalidation mechanism. Pages are still generated as static HTML at build time — but they're also regenerated in the background at a configured interval. After the revalidate window expires, the next request triggers a background regeneration while still serving the cached version. The user never waits for regeneration.

The Stale-While-Revalidate Model

Request 1 (t=0):  Cached HTML served (age: 0s)
Request 2 (t=30): Cached HTML served (age: 30s) ← revalidate = 60
Request 3 (t=61): Cached HTML served (age: 61s) → triggers background regen
Request 4 (t=62): NEW HTML served (just regenerated)

The user at t=61 sees stale content, but the regen happens so fast (typically 1-3 seconds) that request 4 (a second later) already has fresh content.

ISR in Practice

// Next.js App Router ISR
export const revalidate = 3600 // Revalidate at most every 1 hour

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProduct(params.id) // Cached for 1 hour
  return <ProductDetails product={product} />
}

On-Demand ISR: Invalidate by Event

ISR also supports on-demand revalidation — you can purge cached pages immediately when data changes, without waiting for the time-based window:

// 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, secret } = await request.json()

  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 })
  }

  if (tag) {
    revalidateTag(tag) // Invalidate all pages tagged with this key
  }
  if (path) {
    revalidatePath(path) // Invalidate a specific path
  }

  return Response.json({ revalidated: true })
}

Your CMS or backend calls this endpoint via webhook whenever content changes — giving you instant invalidation with static performance.

When ISR Is the Right Choice

  • E-commerce product catalogs — prices and stock change regularly but are the same for all users
  • News and media — articles update hourly; the same article serves all readers
  • Event listings — updated as events are added but identical across users
  • Documentation with a CMS backend — content team publishes and pages update

ISR's Limit: No Personalization

ISR serves the same cached page to every user. If your product page needs to show different prices for logged-in users vs. guests, or if the user's cart badge needs to be accurate, ISR can't help — you need SSR or PPR.


Server-Side Rendering (SSR)

SSR renders the page on every single request. The server fetches data, renders React to HTML, and sends the result to the browser. Every user gets a fresh page, computed specifically for their request.

How It Works

Request time (every request):
  User → Server (origin or Lambda)
  → fetch data (database, API)
  → render React
  → send HTML to browser

SSR in Practice

// Next.js App Router: SSR by default when accessing request-time data
import { cookies } from 'next/headers'

export default async function Dashboard() {
  const cookieStore = await cookies() // This makes the route dynamic (SSR)
  const sessionToken = cookieStore.get('session')?.value

  const user = await getUser(sessionToken)
  const metrics = await getUserMetrics(user.id)

  return <DashboardUI user={user} metrics={metrics} />
}

When SSR Is the Right Choice

  • Authenticated pages — content is unique to each user
  • Real-time data — prices, inventory, live scores that change second to second
  • Checkout flows — cart state must be authoritative
  • Search results — driven by query parameters
  • A/B testing at the server level — where variant must be decided before first paint

SSR's Cost: Server Load and TTFB

Every SSR request hits your origin server and database. At high traffic, this means significant compute cost and TTFB in the 200-500ms range (or higher with slow database queries). CDN full-page caching isn't possible since every response is different.

The mitigation is edge SSR — moving server-side rendering to edge nodes (Cloudflare Workers, Vercel Edge) to reduce the geographic latency component. But edge runtimes have constraints (no native Node.js modules, CPU limits) that limit what queries you can run at the edge.


Partial Prerendering (PPR)

PPR is the newest strategy, stable in Next.js 15 and default in Next.js 16. It elegantly merges SSG and SSR: most of the page is prerendered as a static shell (cached on CDN), while <Suspense> boundaries mark "dynamic holes" that stream in from the server after the shell.

The Core Insight

Most pages have parts that are identical for all users (navigation, product description, article content) and parts that are user-specific (cart badge, personalized pricing, user avatar). Before PPR, you had to choose one strategy for the whole page. PPR lets you choose per component.

// app/product/[id]/page.tsx — PPR in action
import { Suspense } from 'react'

export const experimental_ppr = true // Next.js 15; default in 16

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id) // ← Fetched at build time, in static shell

  return (
    <div>
      {/* Static — in CDN-cached shell */}
      <ProductImages images={product.images} />
      <ProductDescription product={product} />

      {/* Dynamic — streamed after shell */}
      <Suspense fallback={<PricingSkeleton />}>
        <PersonalizedPricing productId={params.id} />
      </Suspense>

      <Suspense fallback={<StockSkeleton />}>
        <LiveStockIndicator productId={params.id} />
      </Suspense>
    </div>
  )
}

The browser receives the static shell (product images, description) in ~50ms from CDN. Then personalized pricing and live stock stream in from the origin server — typically 100-300ms later.

When PPR Is the Right Choice

  • E-commerce product pages — shared catalog content (static) + personal pricing and cart (dynamic)
  • SaaS app shells — navigation and layout (static) + user-specific metrics (dynamic)
  • Content sites with auth — article content (static) + user bookmarks/reading progress (dynamic)
  • Landing pages with user state — marketing content (static) + "Welcome back, [name]" (dynamic)

PPR vs ISR: They're Complementary, Not Competing

A common misconception: PPR replaces ISR. It doesn't — PPR builds on top of ISR. The static shell in a PPR route is itself an ISR-managed artifact. You can apply revalidate and revalidateTag to the PPR shell just like a standard ISR page. PPR adds the ability to stream per-request dynamic content on the same route without degrading the entire page to SSR.

NeedUse
Content same for all users, updates hourlyISR
Content same for all users, updates instantly on publishISR + on-demand revalidation
Content unique per userSSR or PPR
Mixed (shared layout + per-user content)PPR
Currently on Next.js 15 stableISR (PPR requires canary builds in v15)

CSR: The Strategy to Avoid for Content

Client-Side Rendering (CSR) renders nothing on the server — the browser downloads a JavaScript bundle, executes it, fetches data, and renders the page. The initial HTML is empty or minimal.

Request time (CSR):
  User → CDN → empty HTML + JS bundle
  → browser parses JS (200-500ms)
  → fetch data API call (100-500ms)
  → render content
  Total time to content: 500ms-1s+ even on fast connections

CSR is appropriate for: admin dashboards, internal tools, logged-in SaaS views where SEO doesn't matter and the user is on a fast connection. It's a poor choice for any public-facing content page.


Framework Support Matrix

StrategyNext.jsAstroSvelteKitRemix
SSG✅ Default✅ Default⚠️ Limited
ISRrevalidaterevalidate⚠️ Adapter-dependent
SSR✅ Dynamic routesoutput: 'server'✅ Default✅ Default
PPR✅ Next.js 15+
CSR'use client'✅ Islands

PPR is currently Next.js-only. Astro's Island architecture is philosophically similar (static shell + interactive islands) but operates differently — islands add JavaScript interactivity, not server-streamed dynamic HTML.


The Decision Flowchart

Is the page content identical for all users?
  ├─ YES → Does it change frequently (hourly or on publish)?
  │          ├─ NO → SSG (build once, serve forever)
  │          └─ YES → ISR (scheduled or on-demand revalidation)
  └─ NO → Does it have a large static section (nav, product info, article body)?
             ├─ YES → PPR (static shell + dynamic holes)
             └─ NO → SSR (fully dynamic, render everything per-request)

Explore rendering strategy support across popular npm packages on PkgPulse.

Related: Partial Prerendering (PPR) Deep Dive · Next.js vs Astro vs SvelteKit 2026 · Next.js Caching Explained

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.