A from-first-principles implementation of all five major web rendering strategies (SSG, SSR, CSR, ISR, and PPR) built with zero frameworks. This project uses only Node.js's built-in http module to showcase the underlying mechanics of modern web rendering without framework magic.
For a deep-dive walkthrough and analysis, check out the full Case Study: Web Rendering Strategies.
Every rendering strategy answers one fundamental question: Where and when does HTML get generated?
WHO generates the HTML?
│
┌────────────┴────────────┐
│ │
SERVER BROWSER
│ │
WHEN? CSR
│
┌─────────┴──────────┐
│ │
BUILD TIME REQUEST TIME
│ │
SSG SSR
│ │
ISR ONLY THE DYNAMIC HOLES
│ │
└─────────┬──────────┘
│
PPR
(both, on the same page,
in the same response)
The core architectural insight is a shared template layer. One templates.js file serves all strategies, but the render functions are invoked at different times and contexts.
rendering-strategies/
├── data/
│ └── posts.json ← Single source of truth
├── dist/ ← Static files generated at build time
│ ├── ssg.html ← SSG output
│ ├── isr.html ← ISR cache output
│ └── ppr-shell.html ← PPR static shell
├── client/
│ └── app.js ← Client-side script for CSR
└── src/
├── server.js ← HTTP server and router
├── build.js ← Build script for static pages (SSG, PPR)
├── templates.js ← SHARED: HTML templates and page components
├── isr-cache.js ← ISR cache states and revalidation timers
└── strategies/
├── ssg.js ← SSG request handler
├── ssr.js ← SSR request handler
├── csr.js ← CSR request handler
├── isr.js ← ISR request handler
└── ppr.js ← PPR request handler (chunked streaming)
- Concept: Generates HTML once at build time. The server acts as a simple file server.
- Analogy: Baking bread the night before opening. Customers get pre-baked bread immediately; no cooking is done at request time.
- Entry Points:
- Build Script: src/build.js
- Request Handler: src/strategies/ssg.js
- Pre-generated Output: dist/ssg.html
- Concept: Generates HTML dynamically on the server for every incoming request.
- Analogy: Made-to-order dishes. Fresh ingredients, cooked on request. High resource utilization but absolute freshness.
- Entry Point:
- Request Handler: src/strategies/ssr.js
- Concept: The server responds with a minimal, blank HTML shell containing a script. The browser downloads the script, fetches data from a JSON API, and constructs the DOM elements dynamically.
- Analogy: Sending the recipe and ingredients to the customer's house so they cook it themselves.
- Entry Points:
- Request Handler: src/strategies/csr.js
- Client-Side Script: client/app.js
- Data Endpoint:
/api/posts
- Concept: Static files are served instantly, but they carry a validity window. Once expired, the request is served stale while triggering a background (asynchronous, non-blocking) rebuild for the next visitor.
- Analogy: Keeping baked goods on the counter. Customers get immediate service. If the item is older than the window, the current customer gets the older item, but it triggers a fresh batch to be made immediately for the next customer.
- Entry Points:
- Request Handler: src/strategies/isr.js
- Cache State & Revalidation: src/isr-cache.js
- Cache Output: dist/isr.html
- Concept: Combines build-time SSG and request-time SSR. A static shell (complete with skeleton placeholders and layout) is served instantly from disk. While the browser is parsing it, the server fetches dynamic data in parallel and streams the second chunk—an inline script—over the same connection to resolve the placeholders.
- Analogy: Handing the customer a bread basket baked this morning immediately, and starting the stove only for the specific dynamic dish they ordered.
- Entry Points:
- Build Script: src/build.js
- Request Handler: src/strategies/ppr.js
- Static Shell: dist/ppr-shell.html
To make the differences between strategies visually obvious, every page includes an "HTML generated at" timestamp baked directly into the document:
- SSG: The timestamp stays frozen across refreshes until you manually run the build script.
- SSR: The timestamp changes to the exact millisecond on every refresh.
- ISR: The timestamp stays frozen during the revalidation window (e.g., 10 seconds). Refreshing after expiration serves the stale page once, then the next refresh shows the updated timestamp.
- CSR: Displays two timestamps: when the shell was sent by the server, and when the browser fetched and rendered the client data.
- PPR: Shows two distinct timestamps in one response: the build-time timestamp on the static layout (frozen), and the request-time timestamp on the trending widget (updates on every request).
You only need Node.js. No external dependencies.
# Build SSG + PPR static files, then start the server
node src/build.js && node src/server.jsOr run them separately:
# Generate the static files
node src/build.js
# Start the server
node src/server.jsVisit the following URLs to compare strategies in action: