From plain HTML to a fully hydrated app, one idea at a time.
"Server-rendered," "statically generated," "single-page app," "hydration": the same handful of ideas keep getting relabelled by every framework that implements them. Underneath all of it is one spectrum: how much of a page gets decided before the request, during the request, or after it lands in the browser. This guide walks that line, and shows where React, Next.js, Create React App, Vite, and webpack each sit on it.
Go top to bottom. The SPA model (ch.3) is deliberately explained before SSR and SSG, because hydration (ch.4), the idea that connects all of them, only makes real sense once you've seen the fully-client-rendered end of the spectrum first. The overview diagram below is in spectrum order, not reading order.
01 The spectrum, at a glance
One axis: how much is decided before the request, versus after it, versus in the browser.
Every site sits somewhere on a line between two extremes. At one end, a server hands the browser a complete page and nothing needs to run afterward for it to work. At the other end, the server hands over almost nothing (an empty shell), and JavaScript running in the browser builds the entire page after the fact. Everything in between (SSG, SSR, hybrid rendering, islands) is a different answer to the same question: when is the HTML actually generated, and how much of the page needs JavaScript before it's actually usable?
This guide walks the line roughly left to right: traditional HTML first, since everything else is a reaction to its limits; then the SPA, which swung all the way to the other end; then hydration, the idea that stitches the two back together; then SSR and SSG, two different answers to "who renders the HTML, and when"; then the hybrid and islands models that mix strategies per route or per component; then where actual tools and frameworks land on this line; then webpack, since bundling is the machinery underneath almost all of it; then a hands-on timeline comparing four of these strategies directly; then a practical decision guide.
02 Traditional multi-page HTML (MPA)
The original model. Still the correct default for a huge share of the web.
Before any client-side framework existed, every navigation was a plain HTTP round trip. Click a link, submit a form: the browser throws away the current page entirely and asks the server for a brand new one. The server queries whatever data it needs, renders a complete HTML document from a template, and sends it back. The browser parses and paints it and the page is done. It's readable, and for links and native form elements, already interactive, with no JavaScript framework involved at all.
| Pros | Cons |
|---|---|
| Simplest mental model; nothing to hydrate | Full reload on every navigation (lost scroll position, flash of white) |
| Fast first paint (nothing to download beyond the HTML/CSS) | No client-side state persists across pages without cookies/sessions |
| Works with JavaScript fully disabled | Each navigation repeats CSS/JS parsing cost from scratch |
| Every URL is a real, crawlable, bookmarkable document | No way to update part of a page without reloading all of it |
This isn't a legacy pattern to escape. It's how large parts of the web still work today: content sites, most e-commerce, and server-rendered frameworks like Rails, Django, Laravel, or a plain Next.js page that opts out of client interactivity entirely. It's the right default whenever a page doesn't need to feel like an app.
03 The SPA model
Ship one HTML shell, then let JavaScript build everything else.
Single Page Applications inverted the model above: the server sends one nearly-empty HTML file (usually just an empty container element and a handful of <script> tags) and a JavaScript bundle downloads, executes, and renders the entire page client-side. Navigating to a different "page" doesn't ask the server for a new HTML document at all; a client-side router intercepts the click, swaps out components, and updates the URL via the browser's History API, with no network round trip for the document itself.
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
Once that JS is running, the framework keeps an in-memory representation of the page (React's "virtual DOM") and, on every state change, diffs the new version against the last one and applies only the minimal set of real DOM mutations needed, instead of asking the server for HTML at all.
Until the JS bundle finishes downloading and executing, the browser has nothing to paint but an empty shell. Slow networks or low-power devices feel this directly: a blank white page, often followed by a loading spinner, then finally the real content. That's noticeably slower time-to-first-content than a server that returns real HTML on the very first response.
| Pros | Cons |
|---|---|
| Instant, app-like navigation once loaded (no full reloads) | The blank-page problem on first load |
| Rich client state persists across "pages" | Poor SEO and slow first paint without extra work |
| Enables things a server-rendered page can't easily do (offline caching, complex client interactions, optimistic UI) | Every visitor pays the full JS-download-and-execute cost up front, even on a fast connection |
04 Hydration, precisely
How the two extremes get stitched back together.
SSR and SSG (next two chapters) both produce real HTML ahead of time, either on the server or at build time, so the browser has something to paint immediately, solving the SPA's blank-page problem. But that HTML is inert: it looks like the finished app, but no event listeners are attached yet and no client-side state exists. Hydration is the step where the same JavaScript that would have rendered the page from scratch in a pure SPA instead runs against the DOM that already exists. It walks that DOM, attaches the event listeners, and reconciles it with the framework's in-memory representation, all without throwing the existing markup away and re-rendering from nothing.
Hydration assumes the DOM it's attaching to is exactly what the framework would have produced itself, given the same inputs. If the server-rendered HTML and the client's first render disagree, the framework hits a hydration mismatch: it may patch the difference, log a warning, or in the worst case throw the existing markup away and re-render the whole thing client-side, quietly paying for both a server render and a full SPA-style render on the same visit.
Common causes of hydration mismatches:
- Non-deterministic values rendered directly:
Date.now(),Math.random(), or anything with a different result on the server than in the browser a moment later. - Browser-only APIs (
window,localStorage) read during the server render, where they don't exist. - Locale or timezone-dependent formatting that differs between the server's environment and the visitor's.
- Markup that branches on
typeof window !== 'undefined', deliberately rendering something different on each side.
The upshot: hydration is what lets a page be both fast to first paint (real HTML immediately) and eventually fully interactive (JS attaches behaviour on top), at the cost of rendering the same content twice: once on the server or at build time, and once again, silently, in the browser.
05 SSR: Server-Side Rendering
HTML generated fresh, on every request.
With SSR, each incoming request runs the page's render logic on the server right then, producing HTML that reflects exactly that request: the logged-in user, today's inventory count, query params, cookies, whatever state exists at that moment. That HTML streams back to the browser and gets painted immediately; the client-side JS bundle then downloads and hydrates it, per chapter 4.
| Pros | Cons |
|---|---|
| Content is always current (no rebuild needed for per-request or personalized data) | The server does real compute on every single request, so cost and latency scale with traffic |
| Good SEO (crawlers see real content immediately) | Time-to-first-byte depends on how slow that request's rendering work is |
| Works without JS for the initial view | A slow database query on the server means every visitor waits for it |
Next.js's App Router pushes this further with Server Components: some components render exclusively on the server and never ship their JavaScript to the client at all, shrinking how much of the page needs to hydrate in the first place (more in chapter 9).
06 SSG: Static Site Generation
HTML generated once, at build time, then served forever, until the next build.
SSG runs essentially the same rendering logic as SSR, but at build time instead of per request, usually once, as part of a build step, well before any real visitor shows up. The output is a set of plain .html files pushed to a CDN, so serving a page becomes a static file lookup: no server compute, no per-request render, just bytes served from the edge closest to the visitor. Hydration still happens exactly as with SSR: the client JS bundle downloads and attaches behaviour to the already-painted markup.
| Pros | Cons |
|---|---|
| Fastest and cheapest possible time-to-first-byte (no compute per request) | Content is only as fresh as the last build |
| Trivially scales to any amount of traffic, since it's just static files | A database change after the build won't show up until the next one runs (unless paired with ISR, next chapter) |
| SEO benefits identical to SSR | Every route with dynamic per-user content needs a different strategy |
speaktoadit.com's own pages are SSG on Amplify: built once at deploy time, served as static files, then hydrated for the interactive bits: the theme toggle, the chat page, the photo carousel. There's no per-visitor server render happening.
07 Hybrid rendering: ISR and per-route strategies
Modern meta-frameworks don't force one answer for the whole site.
Next.js (and comparable frameworks) let you pick a rendering strategy per route rather than for the whole application: a marketing page can be pure SSG, a dashboard can be SSR, and a product listing can use Incremental Static Regeneration (ISR), served statically like SSG but with a revalidation window (for example, "regenerate this page in the background at most once every 60 seconds") so it stays close to fresh without paying a per-request render cost on every visit.
| Strategy | When HTML is built | Freshness | Typical use |
|---|---|---|---|
| SSG | At build time | Until the next deploy | Marketing pages, docs, blog posts |
| ISR | At build time, then periodically revalidated | Stale by at most the revalidation window | Product catalogs, semi-frequently changing content |
| SSR | On every request | Always current | Personalized dashboards, per-user data |
| CSR (SPA) | Never (the client renders) | As current as the client's own fetch | Authenticated, highly interactive in-app views |
This per-route flexibility is arguably the biggest practical difference between "a framework" like Next.js and either a raw SPA setup or a plain SSR framework. You're not locked into one point on the spectrum for the entire application.
08 Beyond full hydration: islands and resumability
Hydrating the whole page isn't the only option, and it isn't always a good one.
Everything so far assumes that, eventually, the client re-runs the whole framework over the whole page: all of it "hydrates." Two newer ideas push past that assumption:
- Islands architecture (popularized by Astro): most of the page is rendered to plain HTML with no client JavaScript at all. Only the specific components that actually need interactivity (a carousel, a like button, a search box) ship their own small bundle and hydrate independently, as isolated "islands" in an otherwise static sea. A page with one interactive widget ships a fraction of the JS a fully-hydrated page would.
- Resumability (Qwik's term): instead of re-running application logic in the browser to reconstruct state the server already computed, the server serializes exactly enough information for the browser to "resume" execution from where the server left off. Event listeners attach lazily, only when a visitor actually interacts, rather than all at once on page load.
Both target the exact cost chapter 4 named: a full hydration pass re-does work the server already did, for the entire page, whether or not most of it is ever interacted with. Islands and resumability are two different answers to "hydrate less" and "hydrate smarter," rather than "hydrate everything, always."
09 Where the frameworks and tools actually sit
Mapping concrete names onto the spectrum above.
React
Ships components and a diffing engine; supplies no router, no bundler, and no built-in rendering strategy. Where a React app lands on the spectrum is entirely a function of what's wrapped around it.
Create React App
react-scripts wrapped webpack and Babel into one command. Output was always a single HTML shell plus a JS bundle, rendered entirely client-side, with no SSR/SSG built in. Now deprecated in favour of framework- or Vite-based setups.
Next.js
Chooses SSG, ISR, SSR, or CSR per route (ch.7). The App Router's Server Components push further, letting some components render only on the server and skip client JS entirely for that part of the tree. What this site is built on.
Vite (+ a router)
A fast dev server and bundler, not a rendering framework, commonly paired with a router for a client-only SPA when a full meta-framework's rendering flexibility isn't needed.
Astro & Remix
Astro defaults to islands (ch.8): HTML-first, JS only where a component asks for it. Remix is SSR-first, built around web-standard forms and per-route data loaders.
The same underlying spectrum (ch.1) explains every one of these: "which framework" is really "which point (or points) on that line does this tool default to, and how much control do you have to move it?"
10 Webpack and bundlers, fundamentals
The machinery underneath almost everything above.
None of SSR, SSG, or SPA rendering would work in a browser without a bundler first. Modern JavaScript is written as many small ES modules (import/export across dozens or hundreds of files, plus packages from node_modules), but shipping hundreds of individual file requests to a browser is slow. A bundler resolves the whole dependency graph starting from one or more entry files and produces a small number of browser-ready output files.
Entry & output
Webpack starts from an entry file, walks every import it finds, and writes the result to output:
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: { filename: 'bundle.js', path: __dirname + '/dist' },
module: { rules: [ { test: /\.tsx?$/, use: 'babel-loader' } ] },
};
Loaders vs. plugins
Loaders transform individual files before they're added to the bundle: compiling a .tsx file, turning a CSS import into injected styles, inlining an image as a data URI. Plugins hook into the wider build process: extracting CSS into its own file, generating the final HTML entry point, minifying output, injecting environment variables.
Dev server & HMR
A dev server keeps a bundle in memory and pushes changed modules to the running page over a live connection (Hot Module Replacement) instead of triggering a full page reload, so component state survives a code edit.
Code-splitting
A dynamic import('./Modal') tells the bundler to put that module in its own chunk, downloaded only when that code path actually runs. This is the mechanism behind route-based lazy loading: only the JS for the page a visitor is actually on has to download up front.
Tree-shaking
Because ES modules declare their exports statically (unlike CommonJS's dynamic require), a bundler can trace which exports are actually imported anywhere and drop the rest from the final output. Importing one function from a large utility library doesn't have to ship the whole library.
Next.js, CRA, and Vite all wrap a bundler rather than replace the concepts above: CRA and classic Next.js used webpack directly, Next.js 13+ can use Turbopack, and Vite uses esbuild and Rollup. Different tool, same job: turn a module graph into a handful of files a browser can load quickly.
11 See it: a rendering-strategy timeline
Pick a strategy, watch where the time actually goes.
Every chapter above described trade-offs in words. This makes them visible: for each strategy, the same handful of milliseconds gets broken down into the stages that produce them, marked where the page's content becomes visible versus when it's actually possible to click anything.
These are hand-set illustrative figures, not measurements of any real deployment: real numbers depend on your server, your bundle size, and the visitor's own network and device, and can vary by an order of magnitude in either direction. One thing is deliberate, though. Stages that represent the same operation across strategies, like the initial network request, are kept at the same duration on purpose, so the only thing that actually changes between bars is what this chapter is about: when and where rendering happens. Where a duration does differ for the same kind of step (SSR's server render is slower than MPA's, SPA's JS bundle is bigger than SSR/SSG's), click that stage on the bar above for why.
12 Choosing a strategy
A practical decision guide, not a purity test.
| Situation | Recommended strategy |
|---|---|
| Marketing site, blog, docs (content barely changes between visitors) | SSG (ISR if it changes occasionally) |
| Personalized or per-user data on every load (dashboard, feed) | SSR |
| Highly interactive, app-like, behind auth, SEO doesn't matter | SPA / CSR |
| Mostly static with a handful of interactive widgets | Islands (Astro or similar) |
| Mixed: some marketing pages, one interactive app section | Hybrid meta-framework, chosen per route |
Most real products end up mixing more than one of these rather than picking a single point on the spectrum for the whole site, which is exactly what chapter 7's hybrid model and chapter 9's framework mapping are for.
13 Glossary & further reading
The vocabulary in one place, plus primary sources.
Quick glossary
| MPA | Multi-page app: every navigation is a fresh server round trip. |
| SPA | Single-page app: one HTML shell, JS renders everything client-side. |
| SSR | Server-Side Rendering: HTML generated per request. |
| SSG | Static Site Generation: HTML generated once, at build time. |
| ISR | Incremental Static Regeneration: SSG with periodic revalidation. |
| Hydration | Attaching behaviour to server/build-produced HTML instead of re-rendering it. |
| Hydration mismatch | Server and client markup disagree; the framework re-renders or warns. |
| Islands architecture | Only specific components ship JS and hydrate; the rest stays static HTML. |
| Resumability | Resuming server-computed state in the browser without re-executing it. |
| Bundler | Resolves a module graph into a small number of browser-ready files. |
| Code-splitting | Breaking a bundle into chunks loaded only when needed. |
| Tree-shaking | Dropping unused exports from the final bundle. |
| HMR | Hot Module Replacement: pushing code changes without a full reload. |
| TTFB | Time to first byte: how long until the response starts arriving. |
Primary sources
- react.dev: React docs, including hydration and Server Components
- nextjs.org/docs: rendering strategies, App Router, ISR
- webpack.js.org: bundler concepts (entry, output, loaders, plugins)
- vitejs.dev: Vite's dev server and build model
- docs.astro.build: islands architecture
- qwik.dev: resumability
- MDN: History API: the mechanism client-side routers build on
You now have the whole line: traditional HTML, the SPA model, hydration, SSR, SSG, hybrid rendering, and islands, plus where React, Next.js, Create React App, Vite, and webpack actually sit on it. Re-read the timeline in chapter 11 with that context; the four bars should read very differently now.