Frontend Rendering — RSC, SSR, SPA, Turbo
Overview
All approaches solve the same problem: show content to users and make it interactive. The difference is where rendering happens and how much JavaScript the browser needs.
1. Traditional Web (Server-Rendered HTML)
Sequence Diagram
Browser Server (Rails)
| |
|--- GET /article/123 ---------->|
| | Renders ERB template
| | Queries DB
| | Builds full HTML
|<-- HTML + small JS -----------|
| |
| Shows HTML immediately |
| Loads like-button.js (3 KB) |
| |
| User clicks Like |
|--- POST /articles/123/like -->|
| | Updates DB
|<-- JSON { count: 43 } --------|
| JS updates button text |
| |- Rendering: Server (Rails ERB, PHP, etc.)
- JS sent: Only what each page needs (you link manually)
- SEO: Perfect — HTML is ready
- Navigation: Full page reload
- Complexity: Lowest
2. Turbo (Turbolinks) — Rails
Sequence Diagram
Browser Server (Rails)
| |
|--- GET /article/123 ---------->|
| | Renders ERB template
|<-- Full HTML -----------------|
| Shows page |
| |
| User clicks link to /article/456
| (Turbo intercepts - NO full reload)
| |
|--- GET /article/456 ---------->|
| | Renders ERB template
|<-- HTML body only ------------|
| |
| Turbo swaps body |
| (smooth, no white flash) |
| |
| User clicks Like |
|--- POST /articles/456/like -->|
| | Updates DB
|<-- Turbo Stream fragment -----|
| (small HTML piece) |
| Replaces like-count element |
| |- Rendering: Server (same ERB templates)
- JS sent: ~20 KB (Turbo library) + page-specific JS
- SEO: Perfect
- Navigation: No full reload (body swap)
- Complexity: Very low
3. SPA (Single Page Application) — Client-Side React
Sequence Diagram
Browser Server
| |
|--- GET / ---------------------->|
|<-- Empty HTML + bundle.js ----| (200 KB JS)
| |
| Downloads bundle.js |
| Executes React |
| React calls useEffect |
| |
|--- GET /api/articles/123 ----->|
| | Queries DB
|<-- JSON { title, body } ------|
| |
| React renders article |
| (User finally sees content) |
| |
| User clicks link to /article/456
| (React Router - no reload) |
| |
|--- GET /api/articles/456 ----->|
|<-- JSON ----------------------|
| React re-renders |
| |- Rendering: Browser (client-side)
- JS sent: Everything (~200 KB+)
- SEO: Bad — Google sees empty page until JS executes
- Navigation: No reload (React Router)
- Complexity: Medium
4. SSR (Server-Side Rendering) — Next.js
Sequence Diagram
Browser Server (Node.js)
| |
|--- GET /article/123 ---------->|
| | Fetches data from API
| | Runs React component
| | Generates HTML string
|<-- HTML + page.js (150 KB) ---|
| |
| Shows HTML immediately (fast!) |
| |
| Downloads page.js |
| React HYDRATES entire page: |
| - Re-runs component |
| - Rebuilds virtual DOM |
| - Compares with real DOM |
| - Attaches onClick to button |
| (Duplicate work for h1, p...) |
| |
| User clicks link to /article/456
| (client-side navigation) |
| |
|--- GET /article/456 chunk --->|
|<-- page456.js (20 KB) --------|
| React renders new page |
| |- Rendering: Server first, then browser re-processes (hydration)
- JS sent: Full page as JS (~150 KB) — duplicates HTML content
- SEO: Good — HTML is ready on first load
- Navigation: No reload (client-side routing)
- Complexity: Medium-High
5. RSC (React Server Components) — Next.js + React 19
Sequence Diagram
Browser Server (Node.js)
| |
|--- GET /article/123 ---------->|
| | Runs Server Component
| | Fetches data
| | Renders h1, p, article (server only)
| | Sees LikeButton is use client
| | Serializes its props
|<-- HTML + LikeButton.js (3KB)-|
| |
| Shows HTML immediately |
| Downloads LikeButton.js only |
| Hydrates ONLY the button |
| (h1, p = untouched, no JS) |
| |
| User clicks link to /article/456
| (client-side navigation) |
| |
|--- GET /article/456?_rsc=1 -->|
| | Server renders page 456
|<-- RSC Payload (3 KB data) ---|
| (NOT JavaScript!) |
| |
| React runtime reads payload |
| Patches DOM (no JS execution) |
| LikeButton already cached |
| (downloads nothing extra) |
| |- Rendering: Server for static parts, browser for interactive parts
- JS sent: Only interactive components (~3-5 KB) + React runtime (~20 KB)
- SEO: Perfect
- Navigation: No reload (RSC payload patches DOM)
- Complexity: High
Comparison
Traditional
- Server renders HTML: Yes
- SEO: Perfect
- JS to browser: 0-5 KB
- Page reload: Full
- Hydration: None
- Duplicate content: No
- Complexity: Lowest
Turbo (Rails)
- Server renders HTML: Yes
- SEO: Perfect
- JS to browser: ~20 KB
- Page reload: No (body swap)
- Hydration: None
- Duplicate content: No
- Complexity: Low
SPA
- Server renders HTML: No
- SEO: Bad
- JS to browser: 200 KB+
- Page reload: No
- Hydration: N/A
- Duplicate content: No
- Complexity: Medium
SSR
- Server renders HTML: Yes
- SEO: Good
- JS to browser: ~150 KB
- Page reload: No
- Hydration: Full page (wasteful)
- Duplicate content: Yes (HTML + JS)
- Complexity: Medium-High
RSC
- Server renders HTML: Yes
- SEO: Perfect
- JS to browser: ~25 KB
- Page reload: No
- Hydration: Interactive parts only
- Duplicate content: No
- Complexity: High
The Evolution
Traditional Web (2000s) — Fast, simple, good SEO. But full page reloads feel clunky.
Turbo/AJAX (2010s) — No page reloads, still server-rendered. But complex interactivity is hard.
SPA (2013+) — Rich interactive UI. But bad SEO, slow first load, heavy JS.
SSR (2016+) — Fixes SEO, fast first paint. But duplicates content, hydrates wastefully.
RSC (2024+) — Minimal JS, selective hydration. But complex, and achieves what traditional web already did.
Business Logic Is Always Server-Side
Regardless of rendering approach, database operations always run on the server. User clicks Like → HTTP request → Server updates DB → Response → UI updates. This is identical in all approaches. Only the UI update part differs.