Saikat
← Back to blog

The Complete Guide to Web Rendering: SSR, CSR, SSG, ISR, RSC, and Streaming Explained

July 25, 2026·16 min read
WebPerformanceNext.jsRenderingArchitecture
Share:
The Complete Guide to Web Rendering: SSR, CSR, SSG, ISR, RSC, and Streaming Explained

Every time someone asks "should I use SSR or SSG?" I know the answer they're actually looking for is none of those, you should use the strategy that fits the content, but the trade school curriculum only taught the first two acronyms, so that's the question they ask. There are seven rendering strategies that matter in production in 2026, and the right one for any given route is rarely the same as the right one for the route next to it.

This is the post I wish existed when I was learning: every strategy, side by side, with what each one does at the HTML/network/JS-runtime level, where it shines, where it falls apart, and a decision tree for picking one. No framework religion, no "best practice" cargo culting — just the trade-offs and how to think about them.

Set the stage first: what "rendering" actually means

Pick any web app. There are three questions that fully describe how it delivers HTML to a browser:

  1. Where does the HTML come from? A server, a CDN, or the user's JavaScript.
  2. When is it generated? At build time, on every request, in the background on a timer, or on demand.
  3. What does JavaScript do with it? Nothing (pure HTML), hydrate it (attach interactivity), or stream the rest as it arrives.

Every rendering strategy is a different combination of those three answers. Once you see them as a 3-axis space, the strategies stop being a long list and start being a small matrix.

The seven strategies

1. CSR — Client-Side Rendering

The user's browser downloads an empty HTML shell, then runs JavaScript that fetches data and assembles the page in the DOM. Almost all React/Vue/Angular apps built before 2020 worked this way.

<!-- index.html — that's literally everything the server sends -->
<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/app.45a3c.js"></script>
  </body>
</html>
// app.tsx — runs in the browser
function App() {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    fetch('/api/posts').then(r => r.json()).then(setPosts);
  }, []);
  return posts.length ? <PostList posts={posts} /> : <Spinner />;
}

The browser: empty page → loads JS → fetches data → renders. The user sees a spinner during that whole pipeline.

Where it shines: heavy interactivity once loaded (dashboards, editors, web apps); apps behind authentication where SEO doesn't matter.

Where it falls apart: SEO, time-to-first-byte, time-to-content, first-paint metrics. Search engines can render JS now, but they rank pages slower and less reliably when all the content is in JS bundles.

Frameworks that default to it: SPA-mode React, Vue 2 + Vue Router, Angular pre-Universal, SvelteKit (CSR option).

2. SSR — Server-Side Rendering

The server runs your components on every request, generates the full HTML, and sends it down. The browser displays it immediately. Then JavaScript loads and hydrates the page — attaching event listeners and making it interactive.

// app/page.tsx (Next.js App Router with dynamic = "force-dynamic")
export const dynamic = "force-dynamic";

export default async function Page() {
  const posts = await db.posts.findMany();     // runs on every request
  return <PostList posts={posts} />;
}

What the browser receives:

<!DOCTYPE html>
<html>
  <body>
    <h1>Latest posts</h1>
    <article><h2>Post title</h2><p>...content...</p></article>
    <article><h2>Post title</h2><p>...content...</p></article>
    <!-- ...hydrated by app.js on the client -->
  </body>
</html>

The browser: HTML arrives → content is visible immediately → JS arrives → hydration → interactive.

Where it shines: content that changes per request (logged-in user pages, search results, dashboards with fresh data); pages where you want search engines and slow connections to see real content immediately.

Where it falls apart: TTFB (time to first byte) suffers because you can't start the response until the database query finishes; server costs scale with traffic; hydration on slow devices can feel like "the page is there but I can't click it."

Frameworks that default to it: Next.js Pages Router (with getServerSideProps), Remix, SvelteKit (default for dynamic routes), Nuxt.

3. SSG — Static Site Generation

The HTML is generated once, at build time, and served as flat files from a CDN. There's no server-side computation on request.

// app/page.tsx — Next.js with no dynamic exports
export default async function Page() {
  const posts = await db.posts.findMany();    // runs AT BUILD TIME
  return <PostList posts={posts} />;
}

After next build, you get files like out/posts/index.html. The CDN serves them as fast as possible — no Node process, no database hit, just a file.

Where it shines: marketing pages, blogs, docs, product pages — anything where content doesn't change per user and you can tolerate a delay when content changes (a redeploy).

Where it falls apart: large sites (think 50k+ pages) take forever to build; personalized or auth-gated content can't be SSG; pricing tiers can balloon because you rebuild on every change.

Frameworks that default to it: Next.js (output: "export"), Astro (default), Eleventy, Hugo, Gatsby, most docs frameworks.

4. ISR — Incremental Static Regeneration

The best of both worlds in concept: pages are statically generated, but the CDN regenerates them in the background after a configurable interval (or on-demand), so content stays fresh without a full rebuild.

// app/posts/[slug]/page.tsx — Next.js
export const revalidate = 60;       // regenerate at most every 60 seconds

export default async function Page({ params }) {
  const post = await db.posts.findUnique({ where: { slug: params.slug }});
  return <Post post={post} />;
}

What happens: the first request after 60 seconds triggers a regeneration in the background. Other visitors keep getting the stale page until the new one is ready — then everyone gets it.

Where it shines: large content sites (news, e-commerce catalogs, docs with millions of pages), where you can't rebuild on every edit but you also can't ship stale data.

Where it falls apart: still no personalization; CDN cache invalidation across regions can lag; the "stale while revalidate" semantics confuse users when they expect instant updates.

Frameworks: Next.js (the default for pages without dynamic = "force-dynamic"), Nuxt with nitro prerendering.

5. SSR Streaming (React Server Components, Next.js App Router default)

The server starts sending HTML before the data is fully loaded, using HTTP/1.1 chunked encoding or HTTP/2 streams. The browser renders what arrives, then keeps filling in as the rest streams in.

// app/page.tsx — server component (default in Next.js 13+ App Router)
import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Header />                                    {/* sends immediately */}
      <Suspense fallback={<Skeleton />}>
        <SlowDataSection />                         {/* streams when ready */}
      </Suspense>
      <Footer />                                    {/* sends immediately */}
    </>
  );
}

What the network looks like:

chunk 1: <html><body><header>...</header>
chunk 2: <div class="skeleton">...</div>      ← fallback while data loads
chunk 3: <section>Real content...</section>   ← replaces skeleton when ready
chunk 4: <footer>...</footer></body></html>

The browser sees the shell in ~50ms, then content streams in as the slow query resolves.

Where it shines: any page where different parts have different latency budgets (e.g., header is fast, recommendations are slow, comments are slower still); the modern default for most React apps.

Where it falls apart: Suspense boundaries force you to think about what should fall back vs. block; not all hosting platforms handle streaming correctly; tooling/libraries that aren't RSC-compatible need workarounds.

Frameworks: Next.js App Router (default), Remix (deferred data), Modern SvelteKit, Astro server islands.

6. RSC — React Server Components

A first-class architectural concept in Next.js 13+: components that only run on the server, ship zero JavaScript to the client, can directly access databases/files/services, and interleave with interactive client components. Streaming SSR is the transport; RSC is the model.

// app/PostList.server.tsx — server component, zero JS shipped
import { db } from "@/lib/db";
import { LikeButton } from "./LikeButton.client";

export default async function PostList() {
  const posts = await db.posts.findMany();     // runs on the server, no fetch round-trip
  return posts.map(post => (
    <article key={post.id}>
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  ));
}
// app/LikeButton.client.tsx — interactive client component
"use client";
export function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  return <button onClick={() => setLikes(n => n + 1)}>♥ {likes}</button>;
}

The server component ships as serialized RSC payload + HTML; only the interactive leaf components ship JS. A blog page with 50 posts ships maybe 1KB of JS instead of a full React bundle.

Where it shines: data-heavy pages where most of the content is server-rendered; any place you'd previously written an "API route + fetch on the server + render" dance; apps where you care about bundle size.

Where it falls apart: mental model is genuinely new (you have to learn "use client" boundaries); third-party libraries are still catching up; debugging is harder because the boundary isn't always obvious.

Frameworks: Next.js 13+ (the primary example), Waku, Parcel 2 with React Server.

7. PPR — Partial Pre-Rendering (experimental, Next.js)

The newest entry on the list: at build time you ship a static shell; on each request you stream only the personalized dynamic parts into the shell. The end result is the best of SSG and SSR.

// app/page.tsx — Next.js with Partial Pre-Rendering
export const experimental_ppr = true;          // route opt-in

export default function Page() {
  return (
    <>
      <Header />          {/* static, in the build shell */}
      <Suspense fallback={<CartSkeleton />}>
        <DynamicCart />   {/* personalized, streamed on request */}
      </Suspense>
    </>
  );
}

What happens:

  1. Build outputs HTML + a static shell with Suspense placeholders.
  2. CDN serves the shell instantly (SSG-fast).
  3. On request, Next streams the dynamic parts into the placeholders (SSR-streaming-fast).
  4. User sees content in ~50ms, personalized bits arrive ~100–300ms later.

Where it shines: e-commerce home pages, news sites with logged-in personalization, any "static chrome + dynamic personalization" pattern.

Where it falls apart: experimental (still in active development); hosting requirements; debugging the boundary is harder than vanilla SSR.

Frameworks: Next.js (experimental flag), a few Cloudflare-flavored WASM stacks experimenting with similar patterns.

The comparison cheat sheet

Strategy HTML origin Generation time JS needed for content Personalization Cacheability Best for
CSR Browser Per request (in JS) Yes ✅ Yes None (just JS bundle) Heavy web apps, dashboards
SSR Server Per request For hydration only ✅ Yes TTL or none Logged-in user pages, fresh data
SSG CDN/build At build For hydration only ❌ No Forever (until rebuild) Marketing, blogs, docs
ISR CDN + rebuilder Build + on-interval For hydration only ❌ No Up to revalidate window Large content sites, e-commerce
Streaming SSR Server Per request, streamed For hydration only ✅ Yes (by Suspense boundary) TTL on the static parts Most modern apps
RSC + Streaming Server Per request, streamed Only for client islands ✅ Yes Static parts cached, dynamic parts per request Data-heavy apps with islands of interactivity
PPR CDN + server Hybrid Only for client islands ✅ Yes (dynamic islands) Static shell forever, dynamic per request Personalization on top of static content

A decision tree

Walk through this from top:

Is the page content the same for every visitor?
├── NO  → Does the data change per request and need to be fresh?
│        ├── YES → SSR or Streaming SSR
│        └── NO  → Can you tolerate a 60-second stale window?
│                  ├── YES → ISR (with a generous revalidate)
│                  └── NO  → SSR
│
└── YES → Do you have > 100k pages?
         ├── YES → ISR (build time becomes a constraint)
         └── NO  → SSG
                  ↓
         Now: do you also have islands of personalization?
         ├── NO  → You're done. SSG.
         └── YES → RSC (server islands) or PPR if you can use experimental Next features

A quick sanity check per page type:

  • Marketing home, /pricing, /about, /blog postsSSG
  • Logged-in dashboardSSR (or Streaming SSR) with RSC for the static chrome
  • E-commerce product pageISR with a short revalidate (60–300s) + dynamic cart/personalized pricing islands
  • E-commerce home with personalized heroPPR (when stable) or RSC + Streaming
  • News articleISR with on-demand revalidation triggered by the CMS
  • Search results pageSSR (every query is unique)
  • Real-time dashboardCSR with WebSocket/SSE for updates, or RSC background revalidation
  • Docs site with millions of pagesISR or pure SSG with build-time data ingestion
  • AI chat appCSR after initial SSR shell (you can't pre-render an LLM response), or SSR streaming with the LLM call streamed

The honest take: you usually mix

The "use SSR everywhere" or "use SSG everywhere" advice is wrong because every page in your app has different requirements. The Next.js App Router was specifically designed around the idea that you pick the strategy per route (or even per Suspense boundary):

// app/layout.tsx
// SSG by default — every child inherits unless overridden

// app/page.tsx                        → SSG (built once)
// app/dashboard/page.tsx               → SSR (export const dynamic = "force-dynamic")
// app/blog/[slug]/page.tsx             → ISR (export const revalidate = 60)
// app/search/page.tsx                  → SSR
// app/(marketing)/page.tsx             → SSG
// app/products/[id]/page.tsx           → RSC + Streaming with <Suspense> for the personalized price block

The mental shift that took me the longest: rendering strategy is a per-route decision, not an app-wide one. Pick what each page needs, mix freely, and let your framework (Next.js, Remix, SvelteKit) handle the orchestration.

Common mistakes (the ones I keep seeing)

A few things people get wrong about rendering strategies — worth calling out before you pick one.

Treating SSG as "free performance." Static doesn't mean zero cost. You pay it in build time, in deploy complexity, and in the architectural constraint that you can never personalize. If you only have 50 pages, SSG is fine. If you have 500k product pages, ISR will save your sanity.

Treating SSR as "always slower than SSG." A small static page rebuilt on every CMS edit will cost you a 2-minute deploy pipeline. A dynamic SSR page with a 30 ms database query is faster to first byte than the CDN round-trip you'd hit waiting for SSG to invalidate. It depends.

Hydrating everything client-side by default. Even with Next.js, devs often wrap components in "use client" they don't need to. Every "use client" boundary is JS shipped to the browser. If a component doesn't need useState, useEffect, or event handlers, it shouldn't be a client component.

Confusing RSC with SSR. They're related but distinct. SSR renders React components on the server to HTML and ships a hydrated client copy. RSC runs on the server, serializes the result, and ships no client copy at all for server-only components. RSC is an architecture choice; SSR is a transport choice. You can have RSC without streaming SSR (Next.js does both).

Ignoring the JavaScript cost. Rendering strategy isn't only about when HTML arrives — it's about how much JavaScript the page loads to become interactive. A CSR page with a 500KB JS bundle and a 5-second fetch will feel slower than an SSG page that hydrates in 200ms. The metric to watch is TTI (time to interactive), not just LCP.

Forgetting about CDN cache control. If you're doing SSR but forgetting to set Cache-Control headers, you're paying for the same render on every request even though the output is identical for half your traffic. SSR + smart caching headers > SSR alone.

The future direction (and where this is heading)

The industry is converging on streaming + islands + boundaries. The reason is that "render everything on the server" is wasteful for content that doesn't change, and "render everything on the client" is wasteful for content that's already known. The architectures that win are the ones that make the trade-off per component, with the framework handling the orchestration.

Three trends to watch:

  • PPR and similar hybrid models. Static shell + dynamic islands, with the boundary managed for you. The default in 5 years.
  • Edge-rendered everything. Compute moves to the edge (Cloudflare Workers, Vercel Edge, Deno Deploy). SSR + ISR + PPR at the edge gets you <50 ms TTFB globally without bespoke infra.
  • RSC spreading beyond Next.js. The component-server model is so productive that other frameworks are adopting it. Look for it in Remix, SvelteKit-style frameworks, and the WASM-based stacks.

If you're starting a new project in 2026, pick Next.js App Router with RSC + Streaming by default. Use SSG for the marketing surface. Use ISR (or PPR when stable) for content that needs freshness. Use CSR sparingly, only for screens where most of the value is the interactivity once it loads. You'll be writing less code, getting better Core Web Vitals, and shipping faster than people still arguing about which single acronym is best.

Quick framework cheat sheet

Framework Default strategy How to switch
Next.js Pages Router SSG getServerSideProps → SSR; getStaticProps w/ revalidate → ISR
Next.js App Router RSC + Streaming SSR export const dynamic = "force-static" → SSG; revalidate → ISR; force-dynamic → SSR
Remix SSR (loaders) headers({ "Cache-Control": "public, max-age=..." }) for ISR-like behavior; deferred data for streaming
SvelteKit SSR by default export const prerender = true → SSG; +layout.ts revalidate config → ISR
Astro SSG export const prerender = false → SSR; server:defer → streaming
Nuxt 3 SSR (universal) nitro.prerender.routes → SSG; routeRules for per-route ISR/SSR/SWR
Gatsby SSG DSG (Deferred Static Generation) → ISR for slow pages

The map is simple: you have N strategies, your framework supports most of them, and you pick per-route. Once you internalize that, you stop thinking of SSR vs. SSG as a binary and start thinking of rendering as a design choice you make deliberately for each page.


That's the whole map. Every rendering strategy is a different combination of where, when, and how much JS. CSR puts it all in the browser. SSG puts it all at build time. SSR runs it per request. ISR caches per request with a TTL. Streaming SSR streams the response. RSC keeps components server-only. PPR combines the cache and the request into one response.

The right answer for your project isn't any single one of these — it's a mix, chosen per route, with the framework doing the orchestration. Once you see it that way, the acronyms stop being a tribal debate and start being tools you pick up and put down as needed.