The Case Against Single-Page Applications
The following numbers provide context. I audited 12 production React SPAs from clients over the past year. Average JavaScript bundle: 387KB gzipped. Average time-to-interactive on a mid-range Android device: 4.2 seconds. Average number of npm dependencies: 1,847.
Every single one of those apps was fundamentally doing the same thing: fetching data from an API, rendering it as HTML, and letting users submit forms. We'd invented this incredibly sophisticated pipeline, JSX transpilation, virtual DOM diffing, state management, hydration, to do what the browser already does natively with <a> tags and <form> elements.
HTMX's thesis is simple and almost offensively obvious: what if we just sent HTML over the wire?
HTMX 2.0: What Actually Changed
HTMX 2.0 (released stable in late 2025) isn't a revolution over 1.x, but the refinements matter. Here's what's actually different:
The hx-on Syntax Overhaul
This one bit me during the migration. The old syntax:
<button hx-post="/api/items"
hx-on::after-request="handleResponse(event)">
Save
</button>
Became:
<button hx-post="/api/items"
hx-on:htmx:after-request="handleResponse(event)">
Save
</button>
The double-colon was causing issues with certain HTML parsers and CSP policies. The new syntax is more explicit and, importantly, plays nicer with Content Security Policy headers that disallow inline event handlers, since HTMX registers them programmatically.
Native WebSocket Extension (Stable)
The WebSocket extension graduated from experimental in 2.0, and it's genuinely excellent:
<div hx-ext="ws" ws-connect="/ws/notifications">
<div id="notification-feed" hx-swap-oob="beforeend">
<!-- Server pushes HTML fragments here -->
</div>
</div>
On the server side (Go example):
func handleWS(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
for notification := range userNotifications(r.Context()) {
html := renderNotificationHTML(notification)
conn.WriteMessage(websocket.TextMessage, []byte(html))
}
}
No JSON serialization. No client-side template rendering. No state synchronization. The server sends a <div> with the notification content, and HTMX drops it into the DOM. That's it.
Server-Sent Events (The Underrated Gem)
For unidirectional real-time data, SSE is almost always the right choice over WebSockets. HTMX 2.0 makes it trivial:
<div hx-ext="sse" sse-connect="/events/dashboard-metrics">
<div sse-swap="cpu-update" hx-swap="innerHTML settle:300ms">
<span class="metric">Loading...</span>
</div>
<div sse-swap="memory-update" hx-swap="innerHTML">
<span class="metric">Loading...</span>
</div>
<div sse-swap="request-count" hx-swap="innerHTML">
<span class="metric">Loading...</span>
</div>
</div>
Each SSE event type maps to a different swap target. The server sends named events with HTML payloads, and each sse-swap attribute catches the event it cares about. I used this pattern for the entire monitoring dashboard, real-time charts and all. The charts themselves are server-rendered SVGs that get swapped in. No Chart.js, no D3 on the client.
hx-boost: Progressive Enhancement Done Right
hx-boost is probably the single feature that sells HTMX to skeptics. Add it to your body tag:
<body hx-boost="true">
And every <a> tag and <form> in your page is automatically "boosted", clicks and submissions are intercepted, the request is made via AJAX, and only the <body> content is swapped. The browser URL updates, history works, back/forward works. Your multi-page app feels like a SPA with zero code changes.
A significant advantage: if JavaScript fails to load (or is disabled), everything still works as normal links and form submissions. That's not a theoretical benefit, I've had clients in regions with flaky mobile connections where JS bundles fail to load 8-12% of the time. Those users still get a fully functional app.
Real Comparison: React vs HTMX for a CRUD Dashboard
I rebuilt the same feature, a paginated, filterable user management table with inline editing, in both React (Next.js 15) and HTMX (Go + templ). Here are the actual numbers:
| Metric | React (Next.js) | HTMX + Go |
|---|---|---|
| JS sent to client | 127KB gzipped | 14KB (htmx.min.js) |
| Time to Interactive (3G) | 3.1s | 0.8s |
| Server response time (p95) | 45ms (API JSON) | 38ms (HTML fragment) |
| Lines of client code | 847 (JSX + hooks) | 12 (vanilla JS for confirm dialogs) |
| Lines of server code | 203 (API routes) | 341 (templates + handlers) |
| Build step | 34s (Next.js build) | 1.2s (Go compile) |
| Dev dependencies | 387 packages | 12 packages |
The tradeoff is clear: you write more server-side code with HTMX, but dramatically less client-side code. Total code? About the same. But the HTMX version has almost no moving parts on the client, which means fewer bugs in the part of the stack that's hardest to debug.
The Pattern That Changed Everything: OOB Swaps
Out-of-band swaps are HTMX's answer to "but what about updating multiple parts of the page?" When the server responds to a request, it can include extra HTML fragments tagged with hx-swap-oob:
<!-- Primary response (swaps into the target) -->
<tr id="user-42">
<td>Updated Name</td>
<td>updated@email.com</td>
</tr>
<!-- Out-of-band: also update the notification count -->
<span id="notification-count" hx-swap-oob="true">3</span>
<!-- Out-of-band: also update the last-modified timestamp -->
<time id="last-modified" hx-swap-oob="true">Just now</time>
One HTTP response updates three different parts of the page. No client-side state management. No Redux. No Zustand. No signals. The server knows the full state, renders all the HTML fragments that changed, and sends them in a single response.
I was skeptical about this pattern until I used it in production. It's absurdly effective for the 90% case.
When HTMX Is the Wrong Choice
I'd be dishonest if I didn't cover the cases where I'd still reach for React:
Collaborative real-time editing, think Google Docs or Figma. The conflict resolution and operational transforms need client-side logic that doesn't map to hypermedia.
Offline-first applications, HTMX needs a server. If your app must work without network, you need a client-side data layer.
Complex drag-and-drop interfaces, Kanban boards, workflow builders. The interaction model is fundamentally client-side.
Dense data visualizations with interactivity, Zooming, panning, brushing on charts. Server-rendered SVGs work for display, but interactive exploration needs client JS.
For everything else, and I mean this seriously, HTMX is not just viable, it's often the better choice.
Migration Strategy: The Strangler Fig
You don't have to rewrite. I migrated the client dashboard incrementally:
- Week 1: Added
hx-boostto the layout. Instant SPA-like navigation, no other changes. - Week 2-3: Converted the simplest CRUD pages to server-rendered HTML with HTMX attributes. Removed their React components.
- Week 4-5: Tackled the complex forms. Used
hx-targetandhx-swapfor inline validation and partial updates. - Week 6: Converted the real-time notification system from a React + WebSocket client to HTMX's SSE extension.
- Week 7: Removed React, ReactDOM, Next.js, and 374 npm packages from the project.
The app was in production the entire time. Users didn't notice the switch.
The Preload Extension: Perceived Performance
One more pattern worth sharing. HTMX's preload extension makes navigation feel instant:
<body hx-boost="true" hx-ext="preload">
<nav>
<a href="/dashboard" preload="mousedown">Dashboard</a>
<a href="/users" preload="mousedown">Users</a>
<a href="/settings" preload="mousedown">Settings</a>
</nav>
</body>
preload="mousedown" fires the request on mousedown (which happens ~100-150ms before the click event). By the time the click registers and HTMX processes the boost, the response is usually already cached. Combined with server response times under 50ms, navigation feels literally instant.
Final Thoughts
HTMX 2.0 isn't going to replace React for every project. But it's forced me to confront an uncomfortable question: how much of the complexity in my frontend stack exists to solve problems that the browser already solved in 1999?
The answer, for most of the apps I build, is "most of it."
The web was designed as a hypermedia system. HTMX just leans into that design instead of fighting it. And after six months of building with it in production, I can tell you: it's not a toy. It's not a step backward. It's a genuinely different (and often better) way to build web applications.
A weekend project is sufficient to evaluate the approach for a given use case.