Skip to main content

Command Palette

Search for a command to run...

SSR vs SSG vs ISR — Part 3: The Hybrid Reality

Updated
7 min read
SSR vs SSG vs ISR — Part 3: The Hybrid Reality

If you read Part 1 and Part 2 carefully, you already know the “pure” forms:

  • Server Side Rendering (SSR) does work during every request.

  • Static Site Generation (SSG) does work once at build time.

  • Incremental Static Regeneration (ISR) does work at build time and occasionally in the background.

In most real-world applications with dynamic data, these strategies aren’t used in isolation.

In reality, production code uses all three deliberately, based on data consistency requirements, performance goals, and user expectations.

This part is less about definitions and more about architectural thinking and practical usage.

Let’s dig in.

The E-Commerce Product Page That Broke My Brain

Imagine this route:

/products/airpods-pro

In Part 1 and Part 2, we treated these rendering modes in isolation.

But a real product page is not a textbook example.

In production, a product page typically contains:

  • Static product metadata (title, description, images)

  • Pricing that changes frequently

  • Discounts or promotions

  • Stock availability that can change in seconds

  • Reviews that grow over time

  • Personalisation (e.g., user-specific pricing)

Now ask yourself:

Should the entire product page be SSR?

Should it be SSG?

Should it be ISR?

If you answered one of the above, you’re still thinking in terms of knobs — not goals.

Let’s shift that.


Performance + Freshness = Partial Responsibility

In production, people care about:

  • First Contentful Paint (FCP)

  • Largest Contentful Paint (LCP)

  • Data freshness

  • Scalability

  • Cost

  • SEO

And any sane team will divide information based on how frequently it changes.

Some parts change once a month.

Others change every second.

Treating them the same is wasteful.

So the real architecture looks like this:

Product Page
├── Static Content (SSG/ISR)
├── Frequent Dynamic Data (Client Fetch / SSR API)
├── Personalised Content (Client Fetch / SSR API)

Let’s unpack this through a real sequence.


Static Layout — Pre-rendered for Speed and SEO

The parts that don’t change often — product title, description, images — go through:

  • SSG if they’re truly static

  • ISR if they need occasional refresh

  • Build time or background regeneration

Example (App Router):

export const revalidate = 60  // only if product info changes periodically

async function fetchProductData(slug) {
  return await getProduct(slug)
}

export default async function Page({ params }) {
  const product = await fetchProductData(params.slug)

  return (
    <div>
      <h1>{product.name}</h1>
      <img src={product.image} />
      <p>{product.description}</p>
      {/* The rest will come later */}
    </div>
  )
}

This section benefits from:

  • Static rendering → very fast initial HTML

  • Improved SEO → search bots see full description

  • Low cost → no work on every request

In DevTools:

  • HTML loads with minimal TTFB

  • No blocking work for static content

This alone increases perceived performance.


Behind the Scenes — Why Static Alone Is Not Enough

But in an e-commerce site, static content is never enough.

Because prices and stock change constantly.

If your static HTML says:

Price: ₹19,999
Stock: Available

But right now:

Price: ₹18,499
Stock: Sold out

Then you have technical SEO correctness issues, conversion loss, or worse — legal issues for incorrect pricing.

That’s when you stop generating HTML and start calling APIs.


Client-Side Fetching: Fresh Data, Fast Static

Once the static HTML is delivered, we fill in the dynamic pieces on the client:

"use client"

import { useEffect, useState } from "react"

function ProductDynamic({ slug }) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    async function fetchLiveData() {
      const res = await fetch(`/api/products/${slug}/live-data`)
      const json = await res.json()
      setData(json)
      setLoading(false)
    }

    fetchLiveData()
  }, [slug])

  if (loading) return <p>Loading latest price...</p>

  return (
    <>
      <p>Price: ₹{data.price}</p>
      <p>Stock: {data.stock}</p>
    </>
  )
}

export default ProductDynamic;
  • Fresh data

  • No blocking HTML generation

  • Instant response

  • Separation of concerns

And this fetch happens in the browser:

Browser → GET /api/products/airpods-pro/live-data

Which means:

  • Price and stock are up to date

  • API returns correct values from DB

  • No server rendering needed for the entire page

  • Only relevant data is fetched

You’ll see this in DevTools:

  • HTML loads instantly

  • Fetch/XHR calls fill in dynamic content after HTML

This is how hybrid actually feels.


The Revalidation API — The Missing Piece

Static content can become stale, and waiting for a timer is often not acceptable.

Imagine:

  • A flash sale just started

  • A discount just went live

  • Inventory suddenly dropped

Waiting 60 seconds for ISR to regenerate is not great.

So production systems often use:

On-Demand Revalidation API

Next.js provides this for exactly this purpose.

Instead of waiting for a 60-second revalidate window, we tell Next.js exactly when to regenerate a page.

Here’s how it looks:

// app/api/revalidate/route.js
import { revalidatePath } from "next/cache"

export async function POST(req) {
  const { slug } = await req.json()

  revalidatePath(`/products/${slug}`)  // regenerate just that page

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

Now when the admin updates the price:

  1. The admin panel updates the DB

  2. It calls:

    await fetch('/api/revalidate', {
      method: 'POST',
      body: JSON.stringify({ slug })
    })
    
  3. Next.js runs the revalidation function, deletes the cached HTML for that path, and the next incoming request triggers regeneration of the page.

  4. All future requests serve fresh static HTML

With this:

  • No waiting for timer

  • No stale pages for long windows

  • Precise control over regeneration

This is how production sites keep static pages fresh without blocking users.


Why Production Sites Use This Hybrid Pattern

Let’s revisit the key trade-offs:

SSR

  • Always fresh

  • Expensive per request

  • Higher TTFB

  • Slow at scale

SSG

  • Very fast

  • Pre-rendered

  • Great SEO

  • Static until next build

ISR

  • Static most of the time

  • Fresh occasionally

  • Low cost

  • Good compromise

Hybrid (SSG/ISR + Client Fetch + Revalidation)

  • Static for layout

  • Fresh dynamic data

  • On-demand regeneration

  • Scales well

  • Fast responses

This pattern aligns with production requirements:

  • Fast initial load

  • Correct content

  • Scalable infrastructure

  • SEO-friendly output

When a new price is set:

  1. Layout was static

  2. Dynamic fetch pulls in real pricing

  3. Revalidate API regenerates static output

  4. No stale content lives long

This is not hypothetical.

This is how teams actually build commerce platforms today.


Observability in DevTools

When exploring this hybrid pattern:

HTML (Doc)

  • Built static

  • Instant load

  • Minimal TTFB

Fetch / XHR

  • Client requests dynamic data

  • Price and stock arrive separately

Revalidation Trigger

  • Revalidate API call

  • You won’t see it in Doc timing

  • But you notice updated HTML next request

This separation is the architectural signature of hybrid systems.


Final Reflection

The real confusion around SSR, SSG, and ISR doesn’t come from the concepts themselves.

It comes from the fact that no real production system can be explained with only one strategy.

It’s not:

“This page is SSR”

or

“This page is SSG”

It’s:

“This page’s layout is static,
 this page’s dynamic content is fetched,
 and we regenerate only when needed.”

That is the hybrid reality.

And it’s only visible when you stop thinking in definitions,

and start thinking about:

  • timelines

  • execution models

  • when work happens

  • how content stays fresh

Which is exactly why we built this series.

The End.