the evolution of web app architecture david abram the web started with perl scripts that took 500ms to say "hello world" now we have react server components that stream ui faster than your brain can parse it somehow we went full circle back to the server let me explain how we got here 2 / 38 the journey CGI → App Servers → MPAs → SPAs → SSR → Islands → RSC
we've been swinging between client and server for 30 years 3 / 38 cgi: common gateway interface (1993) • each request spawns a new process • script outputs html via stdout • no persistent state • slow but simple Browser → Web Server → [Perl Script] → HTML
every request was like rebooting your computer 4 / 38 the pain #!/usr/bin/perl
print "Content-Type: text/html\n\n";
print "<html><body>Hello World</body></html>";
problem: spawning processes is expensive
solution: keep the process alive
5 / 38 app servers & mvc (late 1990s) • long-running processes (php, rails, django) • templates + routing + controllers • shared database connections • session management Browser → App Server → Templates → Database
we discovered that keeping things in memory is faster 6 / 38 mpas: multi-page apps (2000s) • server renders complete html pages • full page reload on every navigation • progressive enhancement with javascript • forms submit, page refreshes Click Link → Request HTML → Server Renders → Full Page Reload
the web felt like a slideshow 7 / 38 the shift question: why reload the entire page when only a small part changes?
answer: xmlhttprequest (ajax)
suddenly we could update parts of the page without reloading 8 / 38 spas: single-page apps (2010s) • client-side router • api communication (json over http) • heavy javascript bundles • frameworks: angular, ember, backbone Browser (SPA) ↔ REST API ↔ Database
we moved all the logic to the browser 9 / 38 the spa problem wins: • smooth, app-like experience • no full page reloads • rich interactivity costs: • 500kb+ javascript bundles • blank screen until js loads • poor seo • slow first paint we shipped the entire application to every visitor 10 / 38 enter react (2013) react didn't invent spas, but it changed how we think about ui • declarative ui • component model • virtual dom • unidirectional data flow • jsx as a ui dsl UI = f(state)
react made ui predictable 11 / 38 react's core insight UI = R(f(s))
where: • s = state
• f = component function (state → jsx)
• R = renderer (jsx → ui)
ui is just a pure function of state 12 / 38 the math behind react set definitions: S — set of all possible states
X — set of all jsx trees
U — set of rendered uis
functions: f : S → X (component transforms state to jsx)
R : X → U (renderer transforms jsx to ui)
composition: UI = (R ∘ f)(s)
deterministic and predictable 13 / 38 visual pipeline s ∈ S f x ∈ X R u ∈ U
State ─────→ JSX ─────→ Virtual DOM ─────────→ UI
same state always produces same ui 14 / 38 react's impurity problem reality: side effects everywhere • useEffect: dom mutations, api calls, subscriptions • useState: stateful updates • useRef: mutable references • event handlers: user actions trigger mutations react manages impurity, but doesn't eliminate it 15 / 38 the extended model UI = R(f(s, effects(s)))
where effects(s) includes:
• network requests • dom manipulation • timers and async operations • global state mutations ▍ Note
▍
▍ react isolates side effects in hooks, making them predictable but not pure
controlled impurity is better than chaos 16 / 38 ssr: server-side rendering (2016) problem: spas are slow to first paint
solution: render react on the server, send html
Server: React → HTML
Browser: Download HTML → Load JS → Hydrate
fast first paint, but still heavy bundles 17 / 38 hydration what is hydration? server sends html, browser loads js and "hydrates" it the process: 1. server renders react to html 2. browser displays html (fast!) 3. browser downloads js bundle 4. react attaches event listeners to existing html users see content fast, but can't interact until hydration completes 18 / 38 the hydration problem • still shipping full application bundle • users see content but can't click • "uncanny valley" of interactivity • all-or-nothing hydration ▍ Caution
▍
▍ hydration can take 5-10 seconds on slow devices
we needed a better way 19 / 38 island architecture (2019) concept: only hydrate the parts that need interactivity
[Static HTML]
├─ Island A (interactive, hydrated)
├─ Island B (interactive, hydrated)
└─ Rest (static, no js)
wins: • ship less javascript • faster time to interactive • progressive enhancement we started being selective about what runs on the client 20 / 38 island limitations • islands can't easily communicate • still need two mental models (templates + components) • data fetching still client → api → server • complex state management between islands we needed to unify the model 21 / 38 react server components (2020) core idea: components that run only on the server
• can fetch data directly (no api layer) • can access databases, filesystems • only ship interactive parts to client • streaming ui • unified component model Server Components (no js) + Client Components (js) = Hybrid
one component model, two execution environments 22 / 38 rsc architecture component type │ runs on │ ships js │ can access ───────────────┼─────────┼──────────┼──────────────────────── server │ server │ no │ db, filesystem, secrets
client │ both │ yes │ browser apis, events
you choose where each component runs 23 / 38 rsc data flow Server:
<ServerComponent> (fetch from db)
<ClientComponent> (interactive)
<ServerComponent> (more data)
</ServerComponent>
Browser:
Receives: HTML + minimal JS for client components
server components render to a special format, not html 24 / 38 streaming traditional ssr: wait for everything, send html rsc: stream components as they're ready Server:
→ Send "skeleton" immediately
→ Stream <UserProfile /> when ready
→ Stream <Comments /> when loaded
→ Stream <Chart /> when computed
users see content progressively, not all at once 25 / 38 the pendulum More Server ←───────────────────────────→ More Client
CGI SSR RSC Islands SPA
rsc centers the pendulum do the work where it makes sense 26 / 38 but wait, there's more rsc solved rendering, but what about mutations? • forms still needed client-side js • api routes still required separate endpoints • data fetching ≠ data mutation • the client/server boundary was still awkward we needed a way to call server code from the client 27 / 38 server functions (2023) also called: server actions • functions that run on the server • called directly from client components • no api routes needed • progressive enhancement with forms • type-safe by default 'use server'
async function createPost(formData) {
const title = formData.get('title')
await db.posts.create({ title })
}
the api layer disappeared 28 / 38 how they work traditional approach: // api/posts.js
export async function POST(req) {
const { title } = await req.json()
return db.posts.create({ title })
}
// component.jsx
async function handleSubmit() {
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title })
})
}
two files, serialization, error handling, types 29 / 38 server functions approach // actions.js
'use server'
export async function createPost(title) {
return db.posts.create({ title })
}
// component.jsx
import { createPost } from './actions'
function Form() {
return (
<form action={createPost}>
<input name="title" />
<button>Create</button>
</form>
)
}
one function, direct call, automatic serialization 30 / 38 progressive enhancement key insight: works without javascript
<form action={createPost}>
<input name="title" />
<button>Create</button>
</form>
• with js: async mutation, no page reload • without js: form submits, page refreshes • same code, graceful degradation we've come full circle to html forms 31 / 38 type safety server functions are fully typed: 'use server'
async function updateUser(id: number, name: string) {
return db.users.update({ id, name })
}
updateUser(123, 'alice') // ✓ type-safe
updateUser('123', 'alice') // ✗ error: expected number
typescript works across the network boundary 32 / 38 security model important: server functions run with server privileges
• can access environment variables • can read from filesystem • can connect to databases directly • must validate all inputs • must check authorization ▍ Caution
▍
▍ never trust client input, even in server functions
33 / 38 the complete picture server components: render ui on server
server functions: mutate data on server
client components: handle interactivity on client
Server Components (read) + Server Functions (write) + Client Components (interact)
a complete model for hybrid apps 34 / 38 closing thought ▍ Tip
▍
▍ the best architecture is the one that does the least unnecessary work
• cgi did too much (spawn processes) • spas did too much (ship everything) • rsc does just enough (hybrid approach) efficiency through selective execution 35 / 38 questions to consider • what parts of your app actually need client-side js? • where is data fetching happening and why? • are you shipping code that only runs once? • could server components simplify your architecture? audit your architecture regularly 36 / 38 the future trends emerging: • more selective hydration • better streaming primitives • compiler-driven optimizations • framework-agnostic server components the evolution continues 37 / 38 thank you david abram crocoder.dev 38 / 38