"We rebuilt our Rails app’s frontend twice—once with Hotwire, once with htmx. The results shocked us."
The frontend world is splitting into two camps:
- Hotwire (Turbo + Stimulus) for Rails-like magic
- htmx for HTML-centric simplicity
Both promise SPA-like interactivity without JavaScript frameworks, but their philosophies lead to wildly different tradeoffs.
Here’s what happened when we stress-tested them in production.
1. The Core Philosophies
Hotwire (The Rails Way)
"Upgrade HTML with minimal JavaScript."
✅ Turbo Streams: Real-time DOM updates over WebSockets
✅ Stimulus: Sprinkle JavaScript behavior
✅ Deep Rails integration
htmx (The HTML-First Way)
"Why write JS when HTML can do it?"
✅ AJAX in HTML attributes: No .js
files for basic interactivity
✅ SSE/WebSocket support: But less turnkey than Turbo
✅ Framework-agnostic
2. The Benchmark Battleground
We tested 3 critical scenarios on a SaaS dashboard:
Test 1: Real-Time Updates
Metric | Hotwire (Turbo Streams) | htmx (SSE) |
---|---|---|
Setup Time | 5 minutes (rails g channel ) |
15 minutes (manual SSE endpoint) |
Latency (P95) | 220ms | 190ms |
Connection Stability | 92% success at 10K users | 98% success |
Verdict:
- Hotwire wins for Rails integration
- htmx wins for raw performance
Test 2: Complex Interactivity
A dynamic form with conditional fields
Hotwire Approach:
<%= form_with model: @invoice, data: { controller: "form" } do |f| %>
<%= f.select :type, ["Hourly", "Fixed"], data: { action: "form#update" } %>
<%= turbo_frame_tag "rate_fields" do %>
<%= render partial: "rate_fields", locals: { f: f } %>
<% end %>
<% end %>
htmx Approach:
<form hx-post="/invoices" hx-target="#form-container">
<select name="type" hx-get="/invoice/fields" hx-target="#rate-fields">
<option value="hourly">Hourly</option>
<option value="fixed">Fixed</option>
</select>
<div id="rate-fields"><!-- Server renders partial --></div>
</form>
Verdict:
- Hotwire requires JavaScript (Stimulus)
- htmx achieves the same with zero JS
Test 3: Error Handling
Metric | Hotwire | htmx |
---|---|---|
Failed request feedback | Silent (requires JS hooks) | Automatic (HTML swap) |
Retry logic | Manual | Built-in (hx-retry ) |
Verdict: htmx handles edge cases more elegantly.
3. Key Tradeoffs
Hotwire Strengths
✅ Batteries-included real-time (Turbo Streams)
✅ Stimulus for gradual JS complexity
✅ Rails generators save hours
htmx Strengths
✅ No JavaScript for basic features
✅ Smaller bundle size (14kb vs. Turbo+Stimulus 45kb)
✅ Works with any backend
When to Choose Hotwire
- You’re all-in on Rails
- Your team knows Stimulus
- You need Turbo Drive for SPA-like nav
When to Choose htmx
- You hate writing JavaScript
- Your app is mostly server-rendered
- You want lighter real-time than WebSockets
4. The Hybrid Approach
We ultimately landed on:
- htmx for CRUD forms and simple interactivity
- Hotwire for real-time features (Turbo Streams)
- Stimulus for complex UI (drag/drop, charts)
<!-- Example hybrid component -->
<div data-controller="chart"
hx-get="/stats"
hx-trigger="load">
<!-- htmx loads data, Stimulus renders chart -->
</div>
5. Migration Tips
From Hotwire to htmx
- Replace
turbo_frame_tag
withhx-target
- Swap Stimulus actions for
hx-
attributes - Keep Turbo Drive for page transitions
From htmx to Hotwire
- Add
data-turbo="false"
to htmx forms - Create channels for real-time updates
- Use Stimulus for JS-heavy elements
"But Our App Uses React!"
That’s okay. Start small:
- Try htmx for one form
- Add Turbo Frames for one real-time feature
- Compare team productivity
Which camp are you in? Team Hotwire or Team htmx? Debate below!
Top comments (0)