"Rendering" is one of those words that means three different things depending on who you ask. Before we walk the timeline, it helps to separate the layers:
- The rendering API — the actual function you call (
ReactDOM.render,createRoot,renderToString…) to turn a component tree into DOM or HTML. - The rendering strategy — where and when the HTML is produced: in the browser, on a server per-request, at build time, etc.
- The hydration model — how a server-produced HTML page becomes interactive on the client. The post is organized around layer 2 (strategies), because that's where the chronological and simple-to-complex story is richest — but each strategy is tagged with all three: an At a glance box names its rendering API and hydration model too. There's also a short section near the end that lists the API methods on their own, since "render a component" can literally mean those. Each strategy also has a Metrics table in web-vitals shorthand (TTFB, FCP, LCP, TBT, INP, CLS) — every acronym in this post is spelled out in the Glossary at the end.
One honest caveat up front: chronology and complexity mostly line up, but not perfectly. Islands and streaming SSR landed around the same time, for example. I've ordered primarily by conceptual complexity and noted the rough dates as we go.
A quick timeline
| Year | Milestone |
|---|---|
| 2013 | React open-sourced — Client-Side Rendering (SPA) |
| 2016 | Next.js popularizes SSR + hydration for React |
| 2017–2020 | SSG goes mainstream (Gatsby, then getStaticProps) |
| 2020 | ISR (revalidate) in Next.js |
| 2021–2022 | Islands architecture (Astro) |
| 2022 | React 18: concurrent rendering, streaming SSR, selective hydration |
| 2023 | React Server Components stable in the Next.js App Router |
| 2024 | React 19: RSC + Actions officially part of React |
| 2023+ | Resumability (Qwik) as the frontier alternative to hydration |
The cast of characters
Across every strategy below, the same handful of players keep showing up — what changes is who does the rendering work, and when. Here's the shared system context they all operate in:
The strategies differ in how much of the rendering happens at the CDN (a content delivery network serving build-time output), the origin server (per request), or the browser itself — and how the data source is reached.
1. Client-Side Rendering (CSR) — the original SPA
The starting point. The server sends a near-empty HTML shell with a <div id="root"> and a script tag. The browser downloads the JS bundle, React boots, and builds the entire DOM on the client.
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")).render(<App />);
This is what you get out of the box with Vite, Create React App (RIP), or any plain single-page-app (SPA) setup.
At a glance
| Aspect | This strategy |
|---|---|
| Originated | 2013 (React's original model) |
| Rendering API | createRoot(...).render() — client |
| Rendering strategy | Client-side render (SPA) |
| Hydration model | None — the browser builds the DOM from scratch |
| Frameworks | Vite, Create React App; any plain SPA setup |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | — static shell from the CDN |
| FCP / LCP | — blank until the JS downloads and runs |
| TBT / INP | — ships and re-executes lots of JS |
Fetching data — in the browser, after the first render (a useEffect, or a data library like React Query/SWR). That's the render → fetch → re-render waterfall.
function Profile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch("/api/user").then((r) => r.json()).then(setUser);
}, []);
if (!user) return <Spinner />;
return <h1>{user.name}</h1>;
}
Infrastructure
CDN: yes, but only as a dumb static file host. There is no origin render server — the CDN never talks to a backend; the browser calls the data API itself.
Data flow
| Pros | Cons |
|---|---|
| Dead-simple deployment: just static files on a CDN, no server runtime. | Slow first meaningful paint: the user stares at a blank page until the JS downloads, parses, and executes. |
| Excellent for rich, app-like, highly interactive UIs — especially behind authentication where SEO doesn't matter (dashboards, editors, internal tools). | Historically poor SEO. Crawlers have improved, but it's still a gamble for content that needs to rank. |
| After the initial load, navigation is instant (no full page reloads). | Bundle size grows with your app, and everything is shipped to every user. |
| Data-fetching waterfalls: the page renders, then asks for data, then re-renders. |
Use it when you're building an app, not a content site, and time-to-first-paint isn't your bottleneck.
2. Server-Side Rendering (SSR) + hydration
Render on the server, hydrate on the client. For each request, the server runs your components to a string of HTML and sends a fully-formed page. The browser paints it immediately, then downloads the JS and hydrates — React walks the existing DOM and attaches event listeners instead of rebuilding it. Popularized for React by Next.js in 2016.
At a glance
| Aspect | This strategy |
|---|---|
| Originated | ~2016 (popularized by Next.js) |
| Rendering API | renderToString (server) + hydrateRoot (client) |
| Rendering strategy | Server render, per request |
| Hydration model | Full — hydrateRoot walks the entire tree |
| Frameworks | Next.js, Remix (or React on your own Node server) |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | — the server renders and fetches per request |
| FCP / LCP | — real HTML on the first response |
| TBT / INP | — full-page hydration on load |
Fetching data — on the server, before the HTML is sent, via a per-request data hook (Next.js getServerSideProps, or a Remix loader). No client waterfall — the data is already in the HTML.
// Next.js Pages Router — runs on the server for every request
export async function getServerSideProps() {
const user = await fetch("https://api.example.com/user").then((r) => r.json());
return { props: { user } };
}
Infrastructure
CDN: only for static JS/CSS — not the HTML. Every page comes from the origin server, which is the one thing that touches the data source. The browser hits the server directly for content.
Data flow
| Pros | Cons |
|---|---|
| Fast First Contentful Paint (FCP) — real content is visible before any JS runs. | You now need a running server (and to pay for it, scale it, and keep it warm). |
| SEO-friendly: crawlers get complete HTML. | Time to First Byte (TTFB) can be worse than CSR — the server has to do work before sending anything. |
| Good for content that changes per-request or per-user (the HTML is fresh every time). | The "uncanny valley": the page looks ready but isn't interactive until hydration finishes. Click too early and nothing happens. |
| Hydration re-runs your component tree on the client, so you pay a CPU cost twice. |
Use it when you need fresh, per-request content and SEO — e-commerce product pages, news, anything personalized but public.
3. Static Site Generation (SSG)
Render at build time, not request time. Instead of generating HTML per request, you generate it once when you deploy. The output is a folder of plain HTML files that sit on a CDN. Gatsby pushed this hard around 2017; Next.js formalized it with getStaticProps in 2020.
This is exactly the model behind an Astro site or a static Next.js export.
At a glance
| Aspect | This strategy |
|---|---|
| Originated | ~2017 (Gatsby) |
| Rendering API | Build-time renderToString (or React 19 prerender) + hydrateRoot |
| Rendering strategy | Render once, at build time |
| Hydration model | Full — hydrated in the browser on load |
| Frameworks | Gatsby, Next.js, Astro, Docusaurus |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | — prebuilt HTML from the edge |
| FCP / LCP | — content in the first response |
| TBT / INP | — still full hydration |
Fetching data — once, at build time (Next.js getStaticProps, a Gatsby GraphQL query, or an Astro top-level await). The data source is never touched at request time.
// Next.js Pages Router — runs once, at build time
export async function getStaticProps() {
const posts = await getPosts();
return { props: { posts } };
}
Infrastructure
CDN: yes, and it serves the HTML itself. At runtime there is no origin server at all — the server-side work happened once at build time. The data source is only ever touched during the build.
Data flow
| Pros | Cons |
|---|---|
| The fastest possible delivery: pre-built HTML served from the edge, no server compute per request. | Build time scales with the number of pages. Tens of thousands of pages = painful builds. |
| Cheap and secure — there's no server runtime to attack or scale. | Content is frozen at build time. To update it, you rebuild and redeploy. |
| Great SEO and FCP. | A poor fit for highly dynamic or per-user content. |
Use it when content changes infrequently and is the same for everyone — blogs, docs, marketing sites, landing pages.
4. Incremental Static Regeneration (ISR)
SSG that refreshes itself. Introduced in Next.js in 2020, ISR keeps the build-time static model but lets individual pages regenerate in the background after a configurable interval (revalidate). You get static speed without rebuilding the whole site for every content change.
At a glance
| Aspect | This strategy |
|---|---|
| Originated | 2020 (Next.js 9.5) |
| Rendering API | Same as SSG — build/regeneration renderToString + hydrateRoot |
| Rendering strategy | Build-time render + background revalidation |
| Hydration model | Full |
| Frameworks | Next.js (and hosts that emulate it, e.g. Netlify) |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | — cached HTML from the edge |
| FCP / LCP | — like SSG |
| TBT / INP | — full hydration |
| Freshness | Bounded by the revalidate window |
Fetching data — the same build-time fetch as SSG, plus a revalidate window that lets the page rebuild in the background.
// Next.js Pages Router — like SSG, but re-runs at most once per window
export async function getStaticProps() {
const posts = await getPosts();
return { props: { posts }, revalidate: 60 }; // seconds
}
Infrastructure
CDN: yes, serving the HTML — but unlike SSG, the CDN and origin server talk to each other. When a cached page goes stale, the CDN triggers the server to regenerate it, and the server writes the fresh HTML back into the cache. This bidirectional CDN↔server link is what makes ISR distinct.
Data flow
| Pros | Cons |
|---|---|
| Static-level performance with content that can stay reasonably fresh. | Framework and host lock-in (it depends on the platform's caching infrastructure). |
| No full rebuilds — only the pages that need updating regenerate. | More moving parts: you have to reason about cache states and revalidation windows. |
| Scales to huge sites that would be impractical to build all at once. | The first visitor after a window expires may still get a stale page while the new one builds. |
Use it when you have a large, mostly-static site whose content updates on a predictable cadence — large blogs, catalogs, listings.
5. Islands architecture (partial hydration)
Mostly-static HTML with small interactive "islands." Instead of hydrating the entire page, you ship a static HTML document and hydrate only the specific components that need interactivity — each one independently. Popularized by Astro (2021–2022); the term comes from Katie Sylor-Miller and Jason Miller.
This is the model behind an Astro site built with a few React components sprinkled in: the page is HTML, and only your interactive widgets carry JS.
At a glance
| Aspect | This strategy |
|---|---|
| Originated | ~2019 (term) · 2021 (Astro) |
| Rendering API | Static render at build + per-island hydrateRoot |
| Rendering strategy | Static HTML + partial hydration |
| Hydration model | Partial — only interactive islands hydrate, each independently |
| Frameworks | Astro (also Fresh, Marko) |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | — static HTML from the CDN |
| FCP / LCP | — HTML-first |
| TBT / INP | — only islands ship JS |
Fetching data — page-level data is fetched at build (or on the server) for the static HTML; an island can also fetch on the client when it hydrates. In Astro, the frontmatter runs at build:
---
// Astro frontmatter — runs at build time
const products = await fetch("https://api.example.com/products").then((r) => r.json());
---
<ProductList client:visible products={products} />
Infrastructure
CDN: yes, serving the HTML — same runtime shape as SSG (no origin server in the request path). The difference from SSG is on the browser side, not the infrastructure: only the island bundles are hydrated, so far less JS is downloaded from the CDN.
Data flow
| Pros | Cons |
|---|---|
| Drastically less JavaScript shipped — you only pay for the interactive bits. | The interactivity model is more constrained: islands are isolated and don't easily share live state across the page. |
| Excellent FCP and SEO, like SSG, but with selective interactivity. | A different mental model from a "normal" SPA — you think in terms of static-by-default with explicit interactive opt-ins. |
| Framework-agnostic at the island level (React here, Svelte there, all on one page). | Less suited to apps that are interactive everywhere (an island-heavy page loses the benefit). |
Use it when you have a content-first site that needs a few interactive pieces — marketing pages, docs, or blogs where most of the page is static.
6. Streaming SSR + selective hydration
Send HTML in chunks; hydrate in priority order. Shipped with React 18 in 2022 via renderToPipeableStream. With <Suspense> boundaries, the server streams the parts of the page that are ready now and flushes slower sections (waiting on data) as they resolve. On the client, React hydrates interactive regions selectively, prioritizing the parts the user is touching.
At a glance
| Aspect | This strategy |
|---|---|
| Originated | 2022 (React 18) |
| Rendering API | renderToPipeableStream (Node) / renderToReadableStream (Web) + hydrateRoot |
| Rendering strategy | Server render, streamed in chunks |
| Hydration model | Selective — priority-ordered, progressive |
| Frameworks | Next.js, Remix (React 18 streaming) |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | — the shell flushes before data resolves |
| FCP | — the shell paints immediately |
| LCP | Fills in as slow chunks stream |
| TBT / INP | Improved by selective hydration |
Fetching data — on the server, but wrapped in a <Suspense> boundary: the shell flushes immediately and each slow region streams in when its data resolves.
<Suspense fallback={<Spinner />}>
{/* SlowFeed awaits its own data — it suspends, then streams in */}
<SlowFeed />
</Suspense>
Infrastructure
CDN: only for static assets — like plain SSR, the HTML is dynamic and comes from the origin. The extra requirement here is a streaming-capable server/host; a CDN in front can pass the stream through but can't cache it.
Data flow
| Pros | Cons |
|---|---|
| Faster TTFB — you don't wait for the slowest data source before sending anything. | More complex to reason about, and your data layer needs to play nicely with Suspense. |
| Progressive rendering: the shell appears immediately, slow sections stream in. | Requires streaming-capable hosting/infrastructure. |
| Selective hydration means a slow component no longer blocks the whole page from becoming interactive. | Debugging streamed, partially-hydrated pages is harder than a plain SSR page. |
Use it when you have SSR pages with mixed fast/slow data and you want the fast parts to show and become interactive without waiting on the slow ones.
7. React Server Components (RSC)
Components that run only on the server and ship zero JS. The big paradigm shift. Stable in the Next.js App Router from 2023, and officially folded into React with React 19 (2024). Server Components render on the server, can access your backend/database directly, and send a serialized result to the client — not a JS bundle. You interleave them with Client Components ("use client") for the interactive parts.
// A Server Component — runs on the server, ships no JS to the browser
async function Posts() {
const posts = await db.post.findMany(); // direct data access
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
At a glance
| Aspect | This strategy |
|---|---|
| Originated | ~2020 (introduced) · 2023 (stable) |
| Rendering API | RSC serializer + streaming SSR; hydrateRoot for Client Components only |
| Rendering strategy | Server Components + Client Components |
| Hydration model | Partial — only "use client" components hydrate |
| Frameworks | Next.js App Router (also Waku, RedwoodJS) |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | Per request, but cacheable |
| FCP / LCP | — server-rendered HTML |
| TBT / INP | — Server Components ship zero JS |
Fetching data — right inside the async Server Component, with no API layer in between: await your database or an upstream fetch directly (as in the component above). Caching is per-request via fetch options (no-store, next: { revalidate }), so a single component can be static, dynamic, or revalidated on its own.
Infrastructure
CDN: for the client-component JS and static assets. The RSC payload comes from the origin server, which reads the database directly (no separate API layer). The CDN doesn't talk to the server; the browser fetches from each independently.
Data flow
| Pros | Cons |
|---|---|
| Server Components ship zero JavaScript to the client — smaller bundles by default. | A genuinely new mental model — the server/client split, serialization rules, and what can cross the boundary all take time to internalize. |
| Direct, secure data access (no API layer needed for the server parts; secrets stay server-side). | Server Components can't use state, effects, or browser-only APIs (no useState, no event handlers). |
| Automatic code-splitting at the server/client boundary, and data fetching moves up the tree, killing client waterfalls. | Strongly framework-dependent today (Next.js App Router, and a growing few). The ecosystem is still maturing. |
Use it when you're building on a framework that supports it and want minimal client JS with first-class data access — increasingly the default for new full-stack React apps.
8. Resumability (the frontier) — honorable mention
Skip hydration entirely. Pioneered by Qwik (1.0 in 2023). Instead of re-running components on the client to "wake up" the page, the framework serializes the application state and event listeners into the HTML, and resumes exactly where the server left off — lazily downloading only the code a user actually triggers.
At a glance
| Aspect | This strategy |
|---|---|
| Originated | ~2021 (Qwik) · 1.0 in 2023 |
| Rendering API | Qwik serialize/resume — no createRoot / hydrateRoot |
| Rendering strategy | Server render + serialize state & listeners |
| Hydration model | None — resumption replaces hydration |
| Frameworks | Qwik / Qwik City |
Metrics
| Metric | This strategy |
|---|---|
| TTFB | Depends — build-time or per request |
| FCP / LCP | — HTML-first |
| TBT / INP | — no hydration replay |
Fetching data — on the server via a route loader; the result is serialized into the HTML and read on resume, so the client never refetches it.
// Qwik City — runs on the server, result serialized into the HTML
export const useProducts = routeLoader$(async () => {
return await getProducts();
});
// in the component: const products = useProducts();
Infrastructure
CDN: yes — it serves both the resumable HTML and the fine-grained code chunks. The HTML can be produced at build time or per request; either way the CDN fronts it, and the browser pulls extra chunks from the CDN only when the user triggers them.
Data flow
| Pros | Cons |
|---|---|
| Effectively no hydration cost, regardless of app size — startup stays cheap even for large apps. | A small ecosystem and a different framework (not React proper). |
| Extremely fine-grained, on-demand code loading. | Newer, less battle-tested, fewer engineers familiar with it. |
It's not React, but it's the clearest answer to "what comes after hydration?", and it's shaping the conversation around React's own future.
One domain, many render paths
The diagrams above draw "CDN" and "Origin Server" as separate boxes, which raises a fair question: if my browser just calls example.com, where does the split actually happen? Do I wire up a CDN and a server myself?
Almost never. From the browser's side there is only one address. DNS resolves example.com to a single anycast IP (one address that routes to the nearest data center) at your host's edge network — Vercel, Netlify, Cloudflare, AWS Amplify, and friends. Every response — static file, server-rendered HTML, API route — comes back from that same origin. That's deliberate: one origin means no CORS (cross-origin request restrictions), one TLS certificate, one DNS record to manage.
So there is a proxy at the edge of the domain — but it's the platform's, not something you run. For each request, that edge router looks at the path and decides where to answer from:
example.com/_next/static/chunk.js → static asset → CDN cache (no code runs)
example.com/blog/hello → prebuilt HTML → CDN cache (SSG / ISR)
example.com/dashboard → dynamic route → serverless/edge function (your server code runs)
Two layers: framework vs. host
The examples above are all hosts. It's worth separating them from frameworks, because they're independent layers that people often conflate:
- The framework runs at build/dev time. It decides how and when each route renders (CSR, SSR, SSG, ISR, Islands, RSC) and emits the build output.
- The host runs at request time. It takes that output and serves it under one domain — CDN cache for static, functions for dynamic.
| Layer | What it does | Examples |
|---|---|---|
| Framework | Decides how/when each route renders and produces the build output | Next.js, Remix, Gatsby, Astro, Docusaurus, Qwik |
| Host / platform | Runs the edge at request time (CDN cache + serverless/edge functions), serving it all under one domain | Vercel, Netlify, Cloudflare Pages, AWS Amplify, GitHub Pages |
The two are largely independent — an adapter usually lets a given framework deploy to a given host. But hosts differ in capability: a static-only host like GitHub Pages can serve CSR/SSG/Islands output but can't run functions, so SSR, ISR, and RSC need a host with a compute layer (Vercel, Netlify, Cloudflare, AWS).
The frameworks (and what they render)
A field guide to the tools named throughout this post — what each one is, the strategy it leans toward, and what else it can do. Most let you opt into other strategies per route.
| Framework / tool | What it is | Default rendering | Also supports |
|---|---|---|---|
| Vite / Create React App (RIP) | Build tool / classic SPA scaffold | CSR (#1) | — (you add a router + data layer yourself) |
| Next.js | Full-stack React framework | Per-route mix; App Router is RSC-first (#7) | CSR, SSR, SSG, ISR, Streaming (#1–#6) |
| Remix / React Router | Full-stack, web-standards framework | SSR (#2), streaming-friendly | Static export; edge runtimes |
| Gatsby | React static-site generator | SSG (#3) | SSR, DSG (deferred static generation) |
| Astro | Content-first, multi-framework site builder | SSG + Islands (#3, #5) | SSR via an adapter |
| Docusaurus | React docs/site generator (this very blog) | SSG (#3) | Client-side hydration only |
| Qwik / Qwik City | Resumable framework | Resumability (#8) | SSR/SSG output, without hydration |
Where the split lives in your source code
Zooming into a single framework: you rarely configure that router by hand. The CDN-vs-server decision is derived from how you wrote each route, and the framework's build step translates it into deployment artifacts. Using Next.js as the example:
| What you write in the route | What it compiles to | Served from |
|---|---|---|
| A plain page, no dynamic APIs | Prerendered HTML at build | CDN (static) |
export const revalidate = 60 | Prerendered + background regen | CDN, refreshed by a function (ISR) |
Uses cookies(), headers(), or fetch(url, { cache: "no-store" }) | Rendered per request | Serverless / edge function |
export const dynamic = "force-dynamic" | Always per request | Serverless / edge function |
A file in /public or an imported asset | Hashed static file | CDN |
At npm run build, the framework emits a manifest: which routes are prerendered HTML (upload to the CDN), which are functions (deploy as serverless/edge), and the routing rules between them. The host's build adapter reads that manifest and configures the edge router for you. That's the "transparent" part — the platform is turning your per-route rendering choices into infrastructure.
The decision is mostly your code; the enforcement is the platform's edge. You can override with explicit config (vercel.json, netlify.toml, next.config.js rewrites/headers, _redirects), but the default is convention over configuration. And the practical upshot ties back to every strategy above: they differ mainly in how much of your traffic the edge can answer straight from cache versus how often it has to wake a function.
No magic: who does what
All the diagrams above show what happens, which can still feel like sorcery. It isn't — every piece has exactly one job, and you (the developer) only touch a few of them. Here's the whole cast, de-magicked.
The responsibility map
| Player | Owns | Does not do |
|---|---|---|
| React (the library) | Turning components into HTML/DOM and defining hydration — createRoot, renderToString, hydrateRoot, the streaming APIs | Decide per-route strategy, build, cache, or deploy anything |
| The framework (Next.js, Astro, Remix, Gatsby…) | Per-route rendering decisions, routing, data-fetching conventions, and the build that emits static files + function bundles | Host your app — it produces artifacts, it doesn't run them in production |
| The build adapter (framework↔host glue) | Translating build output into a host's format — static assets, serverless/edge functions, routing + cache rules | Any rendering logic of its own |
| The host / platform (Vercel, Netlify, Cloudflare, or your own infra) | The edge router, CDN cache, function runtime, TLS, and one domain | Decide how routes render — it just runs what the adapter handed it |
| You (the developer) | Choosing the framework, writing components, picking each route's strategy, cache windows, secrets, and the data source | The plumbing above — most of it is convention + config |
So, is it all the framework? No. React renders, the framework orchestrates, the adapter packages, the host runs, and you make the decisions. No single piece is magic; each has one job.
What you actually do
In practice, your work is short:
- Pick a framework that fits the job (static blog → Docusaurus/Astro; full-stack app → Next.js/Remix).
- Write components. Mark the client/server boundary where the framework needs it (
"use client"in RSC). - Choose each route's rendering — usually by convention: static by default; opt into dynamic with
cookies()/headers()/no-store; or schedule refresh withrevalidate. - Set caching/revalidation and provide server-only secrets via environment variables.
- Point routes at your data source (database/API) from the server side.
- Deploy to a host (managed or your own), then check the build output to confirm which routes went static vs. dynamic.
That's it — you're mostly choosing strategies and wiring data. The framework and host handle the mechanics.
What can go wrong (and how to debug it)
| Symptom | Likely cause | How to debug / fix |
|---|---|---|
| Hydration mismatch warning; UI flickers or resets on load | Server HTML ≠ first client render — non-deterministic output (Date.now(), Math.random(), window, locale/timezone) | The console warning names the node; render the same thing on both sides, or defer client-only bits to useEffect / a "mounted" flag |
window/document is not defined at build or request time | Browser API used during SSR or inside a Server Component | Guard with typeof window !== "undefined", move it to useEffect, or mark the component "use client" |
| A page you expected to be static is slow / uncacheable | Accidentally opted into dynamic (cookies(), headers(), no-store) | Check the framework's build output (Next.js marks routes ○ Static / ƒ Dynamic); remove the dynamic API or make the read static |
| A secret shows up in the browser bundle | Server-only env imported into a client component | Keep secrets in server code / non-NEXT_PUBLIC_ env; audit the client bundle |
| Stale content after an update (ISR) | Still inside the revalidate window, or the cache wasn't purged | Inspect cache headers (age, cache-control, x-vercel-cache) with curl -I; trigger on-demand revalidation |
| Nothing streams; the whole page waits | Data layer isn't Suspense-integrated, or there are no <Suspense> boundaries | Add boundaries around slow parts; confirm the server uses renderToPipeableStream |
| Huge JS bundle, slow interactivity | Over-hydration — too much shipped to the client | Run a bundle analyzer; push work into Server Components or islands |
Debugging toolkit: curl -I <url> for cache/render headers · view-source vs. the live DOM (empty shell = CSR, full HTML = SSR/SSG) · disable JS to see the raw SSR output · the Network tab for TTFB, streamed chunks, and bundle sizes · your framework's build output for the static/dynamic map · Lighthouse for FCP/LCP.
Infrastructure needed
| Strategy | Minimum infrastructure | Compute at request time? |
|---|---|---|
| CSR, SSG, Islands | Static file host + CDN (GitHub Pages, S3 + CloudFront, any CDN) | No — pure static delivery |
| ISR | Static host + CDN plus a function to regenerate pages | Occasionally, on revalidation |
| SSR, Streaming SSR, RSC | CDN for assets + a compute runtime (serverless/edge functions or a Node server) + the router that splits them | Yes — every dynamic request |
| all of the above | A data source (DB/API) reachable from the compute layer, plus DNS + TLS | — |
The rule of thumb: the more dynamic the strategy, the more you need a compute layer, not just a CDN.
Rolling your own edge
You don't need Vercel — you can assemble the same static/dynamic split from primitives. The "edge router" is just a reverse proxy or CDN routing rule that dispatches by path: cacheable/static → object storage; dynamic → a function or server.
Assembling it, piece by piece:
| Piece | How to build it |
|---|---|
| Static assets & prebuilt HTML | Object storage (S3 / GCS / Cloudflare R2) behind a CDN (CloudFront / Cloudflare / Fastly), with long cache headers and hashed filenames |
| Dynamic routes | Serverless functions (AWS Lambda, Cloudflare Workers, Google Cloud Run) or a long-running Node server in a container (Fly.io, Railway, ECS, Kubernetes) |
| The router / one domain | A reverse proxy (Nginx) or your CDN's path rules: /_static/* and cacheable paths → the bucket, everything else → the compute origin. Terminate TLS here and point DNS at it |
| ISR-style revalidation | stale-while-revalidate cache headers plus a function that rebuilds a page and writes the fresh HTML back to the bucket/cache |
| Self-hosting a framework | Most ship a Node/container adapter: Next.js output: "standalone" (or OpenNext for AWS primitives), Astro/Remix Node adapters — run that behind the same CDN |
This is exactly what the managed platforms automate for you. Doing it yourself is more work and more knobs, but there's still no magic — it's a proxy, a bucket, a function runtime, and a cache, all wired to one domain.
The API layer (the literal "render" methods)
If by "rendering a component" you meant the actual function calls, here's how those evolved:
| Method | Role & status |
|---|---|
ReactDOM.render(...) | The original client entry point. Removed in React 19. |
ReactDOM.hydrate(...) | The SSR counterpart for attaching to server HTML. Also legacy. |
createRoot(...) / hydrateRoot(...) | The React 18 replacements that unlock concurrent features. |
renderToString(...) / renderToStaticMarkup(...) | Synchronous server rendering to an HTML string (the latter omits React's bookkeeping attributes, for emails or fully-static output). |
renderToPipeableStream(...) (Node) / renderToReadableStream(...) (Web/Edge) | The React 18 streaming APIs. |
prerender(...) / prerenderToNodeStream(...) | React 19 static APIs that wait for all data before resolving, for prerendering RSC output. |
How to choose (a short decision guide)
| If your situation is… | Reach for |
|---|---|
| Static content, same for everyone | SSG, or Islands if you need a few interactive pieces |
| Static-ish but updates regularly | ISR |
| Per-request or personalized content that needs SEO | SSR, and streaming SSR if data sources are uneven |
| App behind auth, SEO irrelevant | CSR is still perfectly fine, and the simplest thing that works |
| New full-stack app, want minimal client JS | RSC via a supporting framework |
A useful rule of thumb: start with the simplest strategy that meets your constraints, and only reach for more complexity when a real requirement (SEO, FCP, bundle size, data freshness) forces your hand. Each step up this list buys you something, but it costs you infrastructure, mental overhead, or both.
The throughline
The whole arc has been a steady push of work away from the user's device and toward build time or the server — while trying to ship less and less JavaScript. CSR puts everything on the client. SSR and SSG move the initial render off it. Islands and RSC attack the JS bundle directly. Resumability questions whether hydration needs to exist at all.
You don't need to pick one for your whole career, or even your whole app. Modern frameworks let you mix these per-route and per-component — a static marketing page, a streamed dashboard, and an island-powered widget can all live in the same project. The skill isn't memorizing the list; it's matching each page's actual constraints to the cheapest strategy that satisfies them.
Glossary
Every acronym and term used above, in one place.
Rendering strategies
| Term | Meaning |
|---|---|
| CSR — Client-Side Rendering | The browser downloads a near-empty shell + JS and builds the DOM itself. (#1) |
| SSR — Server-Side Rendering | The server renders HTML per request; the client then hydrates it. (#2) |
| SSG — Static Site Generation | HTML rendered once at build time and served from a CDN. (#3) |
| ISR — Incremental Static Regeneration | SSG plus background regeneration after a revalidate window. (#4) |
| Islands — partial hydration | Static HTML with small, independently hydrated interactive regions. (#5) |
| Streaming SSR | Server HTML flushed in chunks via <Suspense>, hydrated selectively. (#6) |
| RSC — React Server Components | Components that render only on the server and ship zero JS. (#7) |
| Resumability | Serialize server state into the HTML and resume on the client without hydration. (#8) |
| DSG — Deferred Static Generation | Gatsby's on-demand build: a page is generated on first request, then cached. |
Concepts & infrastructure
| Term | Meaning |
|---|---|
| Hydration | Attaching React's event listeners and state to server-rendered HTML to make it interactive. |
| SPA — Single-Page Application | An app that renders and routes entirely in the browser after the first load. |
| CDN — Content Delivery Network | A geographically distributed cache that serves static files (and cached HTML) from the edge. |
| Edge | Compute and cache running close to users, at the CDN's points of presence. |
| Serverless / edge function | Server code the host runs on demand per request, with no always-on server. |
| Adapter | Framework↔host glue that packages build output into a specific host's format. |
| Manifest | The build's map of which routes are static HTML vs. functions, plus the routing rules. |
| Suspense | A React boundary that shows a fallback while its children await data or code. |
| RSC payload | The serialized Server-Component output streamed to the client (not a JS bundle). |
| Anycast | One IP address that routes each user to the nearest data center. |
| CORS — Cross-Origin Resource Sharing | Browser rules governing requests to a different origin. |
| TLS | The encryption behind HTTPS; usually terminated at the edge/proxy. |
Performance metrics
LCP, INP, and CLS are Google's Core Web Vitals; TTFB and FCP round out what a Lighthouse run reports.
| Metric | What it measures | Improved by |
|---|---|---|
| TTFB — Time to First Byte | How fast the server/CDN starts responding | CDN/edge caching, SSG/ISR, fast origins |
| FCP — First Contentful Paint | When the first content appears | Server/build-time HTML (SSR/SSG), small critical CSS |
| LCP — Largest Contentful Paint | When the main content is visible | SSR/SSG, image priority + optimization |
| TTI — Time to Interactive | When the page reliably responds to input | Less/deferred JS, partial or selective hydration |
| TBT — Total Blocking Time | Main-thread blocking during load | Smaller bundles, Islands/RSC, code-splitting |
| INP — Interaction to Next Paint | Responsiveness to input (replaced FID in 2024) | Less client JS, lighter event handlers |
| CLS — Cumulative Layout Shift | Visual stability (unexpected shifts) | Reserved space for images/ads, stable fonts |
| FID — First Input Delay | Legacy responsiveness metric, replaced by INP in 2024 | (superseded) |
| Bundle / hydration size | JS shipped and re-executed on the client | RSC, Islands, resumability, tree-shaking |
The trend across the whole post: less client JS → better TBT/INP, and HTML produced earlier (build or server) → better TTFB/FCP/LCP.
