React Render Modes: Client, SSG, SSR, and RSC
Client-side React is still the default, but here's what SSG, SSR, and RSC actually buy you and when to reach for each.
Setting Up to Follow Along
If you want to run the examples locally, here is everything you need before starting.
Node.js v22+ is required. The course was recorded on v22.14 LTS. Anything past v20.16 will work for most of it, but stay on an LTS release to avoid surprises. Use FNM (Fast Node Manager), NVM, or Volta to manage your Node versions. They all do the same job.
There are two repos to be aware of:
- Starter repo: blank projects with just enough scaffolding to run. The first half of the course (server-side React) starts from zero, so you will build everything yourself. The second half (performance) uses pre-configured Vite starter projects so you are not resetting the build config repeatedly.
- Completed repo: full solutions for every section. Use it to compare your work or to get unstuck. There is no prize for not looking.
The repo also includes a small SQLite database (nodes.db) used in the Node.js samples, and a globals.css / style.css for the Next.js and performance projects respectively. You do not need to set those up manually; they are already in place when you clone.
If you are only here to read and understand the concepts, skip the setup entirely. Everything is explained in the posts as we go.
React has four distinct ways to render an application. They are not mutually exclusive. You can mix and match them in the same project. But each one answers a different question, and using the wrong one adds complexity without any payoff.
React 19 is the version this course targets. Despite being recently released publicly, its core features (especially RSC) ran in canary on Facebook and Netflix for roughly two years before the public release, so "new" does not mean unstable.
Start Here: Client-Side React
Client-side React is what you already know. Your server ships an empty HTML page and a JavaScript bundle. The browser downloads the bundle, V8 (or SpiderMonkey) runs it, and React renders the app entirely in the browser.
Browser request
↓
Server returns index.html (empty <div id="root">) + bundle.js
↓
Browser runs bundle.js → React mounts components → UI appearsThis has been the dominant approach for over ten years, and it will continue to be. The key point: everything else is optional.
Start with client-side React on all your projects, unless you already know you need something else. SSR, SSG, and RSC are contextual performance enhancements. Adopt them when you have a measured reason, not because they're new.
Static Site Generation (SSG)
SSG pre-renders your React components to flat HTML files at build time. The server just serves HTML with no JavaScript required to show the initial page.
Build time
↓
React components → renderToStaticMarkup() → static .html files
↓
Deploy HTML to CDN
↓
User requests → CDN returns flat HTML instantlyWhat it's for: Content-heavy sites that don't change per-user: blogs, documentation, marketing pages. The course website you're reading right now is built with SSG via Next.js.
The minimal implementation:
import { renderToStaticMarkup } from 'react-dom/server';
function App() {
return (
<html>
<body>
<h1>Hello from SSG</h1>
</body>
</html>
);
}
const html = renderToStaticMarkup(<App />);
// Write html to disk as index.htmlrenderToStaticMarkup produces pure HTML with no React markers. The browser never loads React at all unless you add it back.
Why you still need a framework: renderToStaticMarkup only turns a component into an HTML string. Routing, asset hashing, image optimisation, dev server, code splitting: React provides none of that. Next.js, Astro, and Gatsby wrap renderToStaticMarkup internally and give you everything else. Build it from scratch once to understand how it works; use a framework for any real project.
Server-Side Rendering (SSR)
SSR renders React on the server per request and sends the resulting HTML to the browser immediately. React then hydrates the server-rendered HTML on the client, attaching event listeners and making it interactive, without re-rendering the DOM.
Browser request
↓
Server renders React → sends full HTML string immediately
↓
Browser shows content (fast first paint)
↓
Browser downloads bundle.js
↓
React hydrates: attaches event listeners to existing DOMThe performance win: The user sees content before the JavaScript bundle is even downloaded. This matters for perceived performance on slow connections.
The tradeoff: SSR adds real complexity. Browser-only APIs (window, document, analytics SDKs) crash on the server. Every utility, every third-party library must be audited for SSR compatibility. And on fast devices and fast connections, SSR is sometimes slower, because the server must finish rendering before sending anything, versus the client starting to render as soon as the bundle streams in.
The core APIs:
// Server
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
// Inject html into your HTML template and send it
// Client
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
// hydrateRoot attaches React to existing HTML without re-renderingrenderToString is simpler and works fine when your app renders quickly. renderToPipeableStream is worth it only when you have slow parts (like a database query inside a Suspense boundary) that would otherwise block the fast parts from reaching the browser.
The rule: Measure before you adopt SSR. It is not a guaranteed win.
React Server Components (RSC)
RSC is the newest model and is fundamentally different from SSR. The confusion comes from the fact that both involve the server, but they solve different problems.
| SSR | RSC | |
|---|---|---|
| What renders on server | The whole page, once, per request | Individual components, always |
| Client gets | Full HTML + hydration JS | Rendered output (never the component code) |
| Server connection | One-shot (renders then exits) | Ongoing (component stays server-side) |
| Interactive state | Yes, after hydration | Not in server components. Use client components. |
| Can query DB directly | Technically yes (messy) | Yes, cleanly |
An RSC is a component that only ever runs on the server. The client never receives the source code for it. It only gets the rendered output. This means:
- You can write
asynccomponents that query a database directly - Secret credentials (DB connection strings, API keys) never leave the server
- The client bundle is smaller. Server components are zero bytes on the client.
// ServerComponent.jsx - runs ONLY on the server
// async is valid here - not possible in client components
export default async function NoteList() {
const notes = await db.query('SELECT * FROM notes');
return (
<ul>
{notes.map(note => <li key={note.id}>{note.body}</li>)}
</ul>
);
}By default in an RSC framework, everything is a server component. You opt in to client-side interactivity with "use client":
// ClientComponent.jsx - runs on the client (and SSR'd once)
"use client";
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Where RSC is worth it: Apps with frequent server data needs (dashboards, feeds, note apps). The ability to co-locate data fetching with the component that renders it, with no API layer in between, is the genuine win.
Where RSC is overkill: Client-heavy apps with little server data, SPAs where the data is already coming from an API you own, or any team without framework support (Next.js, Remix, TanStack Start).
Choosing the Right Mode
Is your content mostly static (blog, docs, marketing)?
→ SSG
Does your app need fast first paint on slow connections
and your team can handle the SSR complexity?
→ SSR
Do your components need to query databases / private APIs
and you're using Next.js or a framework that supports RSC?
→ RSC (+ client components for interaction)
Everything else?
→ Client-side ReactThese modes compose. A Next.js app uses SSR for the initial page load, RSC for data-fetching components, and client components for interactive widgets, all in the same app.
Build This
Pick one of the scenarios below and build a small app that uses the right render mode for it. The goal is to feel where the boundaries are, not to ship something production-ready.
Scenario A: Personal blog. You have five markdown posts. No login, no user-specific content, content changes once a month.
- Set up a Next.js project with
output: 'export'innext.config.js - Read your markdown files at build time and render them to static HTML
- Run
next buildand open theout/folder — every post should be a plain.htmlfile - Deploy the
out/folder to GitHub Pages or Netlify Drop
Scenario B: Product dashboard. Logged-in users see their own data. The data changes every few minutes.
- Build it as a client-side React app (Vite is fine)
- Fetch user data in a
useEffectafter mount - Add a loading skeleton so the user sees something immediately
- Notice how the skeleton flashes on every refresh — this is the tradeoff you accept with client-side fetching
Scenario C: News homepage. Content is the same for everyone but updates every hour. Fast first paint matters.
- Set up a Next.js app (no
output: 'export'this time) - Fetch headlines in
getServerSideProps(Pages Router) or as an async server component (App Router) - Open the page, view source — you should see the headlines in the raw HTML before any JavaScript runs
- Compare the source to Scenario B and notice the difference
After building all three, ask yourself: which one felt the simplest to build? Which one required the most defensive code? That feeling maps directly to the complexity cost each mode introduces.
Practice what you just read.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.