Skip to main content

Command Palette

Search for a command to run...

SSR vs SSG vs ISR — Part 2: The Build (SSG & ISR)

Updated
5 min read
SSR vs SSG vs ISR — Part 2: The Build (SSG & ISR)

In Part 1, I observed SSR the only way that made sense to me — through the Network tab.

Every refresh cost ~800ms.
The browser waited.
The server worked.
The timestamp changed.

It was obvious what was happening.

The expensive work was happening during the request.

So for Part 2, I wanted to answer a different question:

What happens if we move that work somewhere else?


Moving the Work to the Build

For SSG, I created two routes:

/pages-ssg
/app-ssg

They look exactly like the SSR example.

Same artificial 800ms delay.

Same Generated At timestamp.

Same UI.

The only difference is when the data is fetched.

In the Pages Router, I used getStaticProps.

In the App Router, I didn’t add anything special.

And that part surprised me.

Because in the App Router, static rendering is the default.

You don’t opt into SSG.

You opt out of it.

If your route doesn’t use dynamic APIs like cookies() or headers(),

and you don’t force dynamic rendering,

Next.js quietly pre-renders it at build time.

That’s the baseline.


Something Broke — And That’s When It Clicked

Initially, I reused the same fetch call from the SSR example:

await fetch("http://localhost:3000/api/product")

It worked perfectly in SSR.

Then I ran:

npm run build

And it failed.

It wasn’t a subtle failure either.

It just couldn’t connect.

At first, that felt strange.

But then it became obvious.

During build time, there is no server running at localhost:3000.

There are no API routes listening.

There is nothing to respond.

SSG doesn’t execute inside a running Next.js server.

It executes during compilation.

That’s a completely different phase.

So I removed the API call and extracted the logic into a function:

const getProduct = async () => {
  await new Promise((resolve) => setTimeout(resolve, 800));
  return {
    name: "Sample Product",
    price: Math.floor(Math.random() * 1000),
    generatedAt: new Date().toISOString(),
  };
};

Now the same function runs:

  • During request time in SSR

  • During build time in SSG

  • During regeneration in ISR

Same logic.

Different execution phase.

That distinction matters more than any definition.


Observing SSG in Production

After building:

npm run build
npm start

I opened DevTools again.

Network → Doc → Timing.

Then refreshed /pages-ssg.

And this is what I saw:

There was no 800ms wait.

The timestamp didn’t change.

The server wasn’t doing anything during the request.

The expensive work had already happened.

SSG doesn’t make your request faster.

It removes work from the request entirely.

The browser just downloads a pre-generated HTML file.

That’s it.

Dev Mode Lies

If you test SSG in development mode, it won’t look like this.

In development, Next.js prioritises fast refresh and developer experience.

It doesn’t aggressively optimise static output.

So if you want to understand how SSG really behaves:

Always test in production mode.

That was one of the biggest sources of confusion for me.

Then Comes ISR

ISR felt like a compromise.

Not fully static.
Not fully dynamic.

In Pages Router:

export async function getStaticProps() {
  console.log("ISR: Generating static props...");
  return {
    props: {
      product: {
        name: "Sample ISR Product",
        price: Math.floor(Math.random() * 1000),
        generatedAt: new Date().toISOString(),
      },
    },
    revalidate: 10, 
  };
}

In App Router:

export const revalidate = 10

That’s it.

No force-static.

No force-dynamic.

Just a revalidation window.

And again, no call to localhost:3000/api/product.

Because the initial render still happens at build time.

If there’s no server running during build, there’s nothing to call.

That constraint forces you to think differently about where your data comes from.


What ISR Actually Does

Right after build, ISR behaves exactly like SSG.

Fast.

Static.

Stable timestamp.

Then I waited 10 seconds.

Refreshed.

And something interesting happened.

The first request after the revalidate window did not block for 800ms.

It still served the old HTML immediately.

But in the background, regeneration started.

Only after regeneration completed did the timestamp and price update on subsequent requests.

That’s the subtle but powerful difference.

ISR does not render per request.

It renders per interval.

And during regeneration, it serves stale content instead of blocking.

It shifts compute away from users most of the time.


Reframing Everything

At this point, the terminology stopped mattering to me.

What mattered was this:

  • SSR → Work happens during request.

  • SSG → Work happens during build.

  • ISR → Work happens during build and occasionally in the background.

That’s the entire model.

Everything else — performance, scalability, freshness — flows from that decision.

Once you see it in the Network tab,

it stops being abstract.

You can measure it.

You can reason about it.

And most importantly —

You can choose intentionally.


In Part 3, we’ll leave the controlled experiment and look at real applications.

Because in production, almost nothing is purely SSR or purely SSG.

And that’s where the real architectural decisions begin.