The Copy Cat ■ Episode 7
In 1995, PHP rendered HTML on the server and sent it to the browser. The browser displayed it. That was the entire architecture. Terribly straightforward, one must admit.
In 2026, the industry renders HTML on the server, sends it to the browser, then ships 558 KB of JavaScript to rebuild the very same DOM. They call this hydration. One rather wishes they were joking.
The Original
Server-Side Rendering. CGI, PHP, JSP, ASP. The server produced HTML. The browser consumed it. Forms, links, progressive enhancement. No second pass required. Quite dull, really. It simply worked.
The model was straightforward: a request arrived, the server assembled HTML from templates and data, and the browser rendered it. Interactivity came from forms and links. Where JavaScript was needed, it enhanced what was already there. The page was usable before a single script loaded. This was not a design philosophy. It was the only option. And it happened to be correct.
The Detour
Then Single-Page Applications moved rendering to the client.
The server sent an empty <div id="root">
and a JavaScript bundle. The browser downloaded the bundle,
parsed it, executed it, fetched data via API calls, and
finally rendered the page. Performance collapsed. SEO
collapsed. First Contentful Paint times tripled. But the
developer experience was, one is assured, quite pleasant.
The industry noticed SPAs were slow. The fix: render on the server again. But they refused to let go of the framework. So now the server renders HTML, the browser displays it, then the framework downloads, parses the DOM a second time, rebuilds the component tree in memory, and attaches event listeners to every interactive element.
The same page. Built twice. Shipped thrice (HTML, JavaScript bundle, serialised state). The architectural equivalent of building a house, handing over the keys, then demolishing it and rebuilding it while the owner watches.
React v16 formalised the ceremony in 2017 with
ReactDOM.hydrate().
The Triple Data Problem
Hydration does not merely rebuild the DOM. It ships the same information three times:
- The rendered HTML (the page the user sees)
- The JavaScript templates (the component code that knows how to render it)
- The serialised state (JSON data embedded in the page so the framework can reconcile)
Three representations of the same content, downloaded over the same connection, processed by the same browser. Instagram ships 5 MB of JavaScript to display photographs. One suspects the photographs themselves are lighter.
The Cascade of Patches
Each attempt to fix hydration created the next problem. The history reads like a support ticket that keeps being escalated:
Next.js re-invented SSR for React in 2016. Partial hydration followed. Astro introduced islands architecture in 2020, shipping JavaScript only for interactive components. Each iteration was cleverer than the last. None questioned whether the framework should be there in the first place.
The Architect's Confession
Misko Hevery, creator of Angular, built Qwik because hydration is, in his words, "pure overhead." His framework replaces hydration with resumability: the server serialises the application state, and the client resumes execution without replaying it. No re-render. No second pass.
Even the architects who popularised client-side rendering now build tools to escape its consequences. Rather telling, that.
The Modern Answer
Rust. Go. Any language that renders HTML on the server without the ceremonial re-enactment.
Askama for Rust: compile-time Jinja-like templates, type-checked at build, zero JavaScript. No runtime template engine. No virtual DOM. The compiler catches your typos before the server starts.
templ for Go: compiled HTML components with type safety and IDE support. No runtime overhead. No JavaScript required. The template is a Go function. The output is HTML. That is the entire abstraction.
Need device-specific layouts? In my Rust backends (for example, this very site), the standard approach: one detection request sets a cookie. The server knows the client. Every response after that is byte-optimised for the device. No framework. No second pass. No hydration.
Need interactivity? A <script> tag. A
fetch call. An event listener. Not a framework
that rebuilds your page to make a button clickable.
The Irony
The server rendered your page. The browser displayed it. Then JavaScript arrived and insisted on doing it all again. Twenty years of engineering to return to where PHP started. The median JavaScript payload grew from 359 KB in 2019 to 558 KB in 2024. A 55% increase in five years, solving a problem that did not exist until client-side rendering created it.
PHP, for all its critics, never asked the browser to rebuild anything. CGI never demanded a second pass. The original server-side model rendered once and was done. The copy renders twice and calls it progress.
Quite the journey.