# The Ultimate Guide to Rendering in Next.js: SSR, SSG, ISR, CSR, PPR & Cache Explained

# Introduction

If you've been building with **React** or **Next.js**, you've probably heard terms like:

*   SSR
    
*   SSG
    
*   ISR
    
*   CSR
    
*   PPR
    
*   React Server Components
    
*   unstable\_cache
    

At first, they all sound like different ways of saying:

> "Render a webpage."

But they actually solve very different problems.

After building several production Next.js applications, I realized that choosing the wrong rendering strategy can make your application slower, more expensive, or harder to maintain.

This guide explains each rendering method from a practical perspective, when to use it, and how they all fit together.

* * *

# First: React vs Next.js

Before diving into rendering strategies, it's important to understand the difference.

## React

React is **only a UI library**.

By itself, React renders everything in the browser.

```plaintext
Browser
     ↓
Downloads JavaScript
     ↓
Runs React
     ↓
Page Appears
```

This is called **Client-Side Rendering (CSR).**

* * *

## Next.js

Next.js extends React by allowing pages to render:

*   On the server
    
*   During build time
    
*   Incrementally
    
*   Partially
    
*   Or entirely on the client
    

That's why Next.js offers multiple rendering strategies.

* * *

# 1\. Client-Side Rendering (CSR)

```plaintext
User
   ↓
HTML Shell
   ↓
Download JS
   ↓
Fetch API
   ↓
Render UI
```

Everything happens in the browser.

Example:

```tsx
'use client'

const { data } = useSWR('/api/posts')
```

### Pros

*   Great interactivity
    
*   Perfect for dashboards
    
*   Minimal server work
    

### Cons

*   Slower first load
    
*   Poor SEO
    
*   Users wait for JavaScript
    

* * *

## Best For

*   Admin panels
    
*   Internal dashboards
    
*   Authenticated apps
    
*   Highly interactive pages
    

* * *

# 2\. Server-Side Rendering (SSR)

Instead of sending an empty page...

Next.js renders everything on the server **for every request**.

```plaintext
User
    ↓
Server
    ↓
Database
    ↓
HTML
    ↓
Browser
```

Example:

```tsx
export default async function Page() {
  const posts = await getPosts()

  return <Posts posts={posts} />
}
```

Every refresh executes the server code again.

* * *

## Pros

*   Fresh data
    
*   Excellent SEO
    
*   Faster first paint
    

## Cons

*   Higher server cost
    
*   Slower response if database is slow
    

* * *

## Best For

*   News
    
*   Search results
    
*   User profiles
    
*   Personalized pages
    

* * *

# 3\. Static Site Generation (SSG)

Instead of rendering when users visit...

Pages are generated during **build time**.

```plaintext
Build
   ↓
Generate HTML
   ↓
Deploy
   ↓
Serve Static File
```

No server code runs when users visit.

* * *

Example:

```plaintext
npm run build
```

creates

```plaintext
about.html
pricing.html
contact.html
```

* * *

## Pros

*   Extremely fast
    
*   Zero database queries
    
*   Cheap hosting
    

## Cons

*   Content becomes outdated
    
*   Requires rebuilding
    

* * *

## Best For

*   Landing pages
    
*   Documentation
    
*   Blogs
    
*   Marketing sites
    

* * *

# 4\. Incremental Static Regeneration (ISR)

ISR combines SSG with automatic regeneration.

```plaintext
User
    ↓
Cached Page
    ↓
TTL Expires
    ↓
Background Regeneration
```

Example:

```tsx
export const revalidate = 60
```

Meaning:

```plaintext
Generate once

↓

Cache 60 seconds

↓

Regenerate automatically
```

Users always receive a cached page while regeneration happens in the background.

* * *

## Pros

*   Almost static performance
    
*   Fresh content
    
*   Lower server costs
    

* * *

## Best For

*   Ecommerce
    
*   Product pages
    
*   Restaurant menus
    
*   CMS websites
    

* * *

# 5\. Partial Prerendering (PPR)

One of the newest rendering strategies in Next.js.

Instead of choosing between SSR and SSG...

You can mix both.

```plaintext
Static Header

↓

Static Hero

↓

Static Footer

↓

Dynamic Products

↓

Dynamic Notifications
```

Static parts are served immediately.

Dynamic sections stream in later.

* * *

## Benefits

*   Faster loading
    
*   Better Core Web Vitals
    
*   Smaller TTFB
    
*   Great UX
    

* * *

## Best For

Large pages with only a few dynamic sections.

* * *

# React Server Components (RSC)

Server Components aren't a rendering strategy.

They're a React feature.

They:

*   Run only on the server
    
*   Never ship JavaScript
    
*   Can access databases directly
    

Example:

```tsx
export default async function Products() {
  const products = await db.product.findMany()

  return <ProductList products={products} />
}
```

No API route needed.

* * *

# Where Does unstable\_cache Fit?

This is probably the most misunderstood feature.

It **does not render pages**.

Instead...

It caches expensive server functions.

Without cache:

```plaintext
Request

↓

Database

↓

Response

↓

Database Again

↓

Response
```

With cache:

```plaintext
Request

↓

Cache

↓

Response
```

Example:

```ts
const getRestaurants = unstable_cache(
  async () => prisma.restaurant.findMany(),
  ['restaurants'],
  {
    tags: ['restaurants']
  }
)
```

Now the database isn't queried every request.

Instead, Next.js serves cached results.

* * *

## Cache Invalidation

Instead of waiting for time...

You can invalidate cache immediately.

```ts
revalidateTag('restaurants')
```

Perfect after:

*   Creating a restaurant
    
*   Updating a menu
    
*   Deleting content
    

* * *

# How Everything Fits Together

```plaintext
                React
                  │
          Client Rendering
                  │
              Next.js
                  │
 ┌─────────────────────────────────┐
 │ CSR │ SSR │ SSG │ ISR │ PPR │
 └─────────────────────────────────┘
                  │
        React Server Components
                  │
          unstable_cache
                  │
           Database / APIs
```

* * *

# Which Framework Uses What?

| Framework | CSR | SSR | SSG | ISR | PPR |
| --- | --- | --- | --- | --- | --- |
| React | ✅ | ❌ | ❌ | ❌ | ❌ |
| Next.js | ✅ | ✅ | ✅ | ✅ | ✅ |
| Remix | ✅ | ✅ | Limited | ❌ | ❌ |
| Astro | Partial | ✅ | ✅ | Partial | ❌ |
| Nuxt | ✅ | ✅ | ✅ | Partial | ❌ |
| SvelteKit | ✅ | ✅ | ✅ | Partial | ❌ |

* * *

# Which One Should You Use?

| Scenario | Recommended |
| --- | --- |
| Admin Dashboard | CSR |
| Marketing Website | SSG |
| Blog | ISR |
| News Website | SSR |
| Ecommerce | ISR |
| User Profile | SSR |
| Documentation | SSG |
| Restaurant Menu | ISR |
| SaaS Dashboard | CSR |
| Hybrid Homepage | PPR |

* * *

# Common Mistakes

❌ Using SSR for everything

❌ Using CSR for SEO pages

❌ Rebuilding for every CMS update

❌ Forgetting cache invalidation

❌ Calling the database repeatedly instead of caching

* * *

# Where Does SWR Fit?

One thing that confused me when learning Next.js was **SWR**.

At first, I thought it was another rendering strategy like SSR or ISR.

It isn't.

**SWR is a client-side data fetching library** created by Vercel.

Think of it as a smart cache for API requests **inside the browser**.

Example:

```plaintext
'use client'

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(res => res.json())

export default function Users() {
  const { data, error, isLoading } = useSWR('/api/users', fetcher)

  if (isLoading) return <>Loading...</>

  return <div>{data.name}</div>
}
```

Instead of manually doing:

```plaintext
useEffect(() => {
  fetch(...)
}, [])
```

SWR automatically handles:

*   ✅ Client-side caching
    
*   ✅ Request deduplication
    
*   ✅ Background revalidation
    
*   ✅ Automatic refetch on window focus
    
*   ✅ Retry on network failure
    
*   ✅ Optimistic UI updates
    

* * *

## SWR vs unstable\_cache

This is how I remember it:

| Feature | `unstable_cache` | SWR |
| --- | --- | --- |
| Runs on | Server | Browser |
| Purpose | Cache expensive database/API functions | Cache API requests on the client |
| Used for | Prisma queries, external APIs, expensive computations | Dashboards, user profile, live UI |
| Invalidated by | `revalidateTag()` / `revalidatePath()` | `mutate()` |
| Part of rendering? | ❌ No | ❌ No |

* * *

## A Simple Mental Model

Whenever I forget where each fits, I remember it like this:

```plaintext
Database
      │
      ▼
unstable_cache
(Server Cache)
      │
      ▼
SSR / ISR / SSG / PPR
(Render HTML)
      │
      ▼
Browser
      │
      ▼
SWR
(Client Cache)
      │
      ▼
User Interface
```

* * *

## Rule of Thumb

*   **SSR** → Render fresh HTML on every request.
    
*   **SSG** → Render once during build.
    
*   **ISR** → Static pages that automatically regenerate.
    
*   **CSR** → Render everything in the browser.
    
*   **PPR** → Mix static and dynamic rendering.
    
*   **React Server Components** → Server-only React components.
    
*   `unstable_cache` → Cache expensive server functions.
    
*   **SWR** → Cache and revalidate API requests in the browser.
    

* * *

# Final Thoughts

There isn't a "best" rendering strategy.

Each solves a different problem.

A modern Next.js application often combines several of them:

*   **SSG** for static pages
    
*   **ISR** for content updates
    
*   **SSR** for personalized experiences
    
*   **CSR** for interactive dashboards
    
*   **PPR** for hybrid performance
    
*   **React Server Components** for efficient server logic
    
*   `unstable_cache` to reduce database load
    

Understanding when to use each is what separates a working application from a fast, scalable one.

Happy building! 🚀

* * *

## Useful Resources

*   Next.js Rendering Docs: https://nextjs.org/docs/app/building-your-application/rendering
    
*   React Server Components: https://react.dev/reference/rsc/server-components
    
*   Next.js Caching Guide: https://nextjs.org/docs/app/guides/caching
    
*   unstable\_cache API: https://nextjs.org/docs/app/api-reference/functions/unstable\_cache
