DEV Community

Cover image for Choosing SSR, ISR, or CSR in Modern Next.js
Rizky Haksono
Rizky Haksono

Posted on Edited on

Choosing SSR, ISR, or CSR in Modern Next.js

The SSR/ISR/CSR comparison is useful, but the usual explanation often turns it into three competing page types. In modern Next.js, they are better understood as decisions about where data is fetched, when work happens, and what can be cached.

I use this question first: who needs the data, and how fresh must it be?

Server rendering for request-time data

Render on the server when the response depends on the current request: authentication, cookies, headers, or data that must be fresh.

export default async function AccountPage() {
  const account = await fetch("https://api.example.com/account", {
    cache: "no-store",
  }).then((response) => response.json())

  return <h1>Welcome, {account.name}</h1>
}
Enter fullscreen mode Exit fullscreen mode

The browser receives useful HTML, but the server performs the work for each request. Use that freshness deliberately; do not disable caching everywhere by habit.

Cached server rendering and revalidation

For documentation, product pages, or articles, the same result can often be shared between visitors and refreshed periodically.

export default async function ProductPage() {
  const products = await fetch("https://api.example.com/products", {
    next: { revalidate: 3600 },
  }).then((response) => response.json())

  return <ProductList products={products} />
}
Enter fullscreen mode Exit fullscreen mode

This is the modern equivalent of the problem ISR solves: serve cached output quickly, then revalidate it according to the application's freshness requirements. For content-driven updates, tag-based or on-demand revalidation can be clearer than an arbitrary timer.

Client-side fetching

Fetch in the browser when the data belongs to an interaction after the page loads: autocomplete results, a live chart, or a user-triggered refresh.

"use client"

import useSWR from "swr"

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

export function LiveStatus() {
  const { data, isLoading } = useSWR("/api/status", fetcher, {
    refreshInterval: 10_000,
  })

  if (isLoading) return <p>Checking status…</p>
  return <p>{data.message}</p>
}
Enter fullscreen mode Exit fullscreen mode

CSR is not automatically faster. It can mean sending an empty shell, downloading JavaScript, and making another request before the user sees the content.

The practical decision

Need Start with
Request-specific or uncached data Server rendering
Shared content that changes occasionally Cached server rendering + revalidation
Interaction-owned or continuously updating data Client fetching

A single page can use all three. The product description may be cached, account pricing may render per request, and a stock indicator may update in the browser.

What I default to

I keep data and rendering on the server unless an interaction genuinely needs client state. Then I add the smallest client component around that interaction. This usually ships less JavaScript and makes the loading path easier to reason about.

The useful question is not “Which acronym wins?” It is “What is the cheapest place to do this work while meeting the freshness and interaction requirements?”

Top comments (0)