⚡ Rails × Hotwire は魔法ではない:設計思想・アンチパターン・スケールの現実 – Part I

⚡ Rails × Hotwire は魔法ではない:設計思想・アンチパターン・スケールの現実 – Part I

· · 5分で読めます

Advertisement

Hotwire は Rails に「SPA の操作感」を持ち込んだ。
これは間違いなく革新だ。

しかし、SPA の複雑さまで一緒に持ち込む必要はない。
むしろ Hotwire を「魔法」だと思って雑に使えば、SPA より重く、デバッグしづらく、スケールしない構成になる。

この記事では、

を、アンチパターン中心に整理する。


1️⃣ よくある Hotwire の最初の一歩(そして罠)

# app/controllers/products_controller.rb
def index
 @products = Product.all
end
<%= turbo_frame_tag "products" do %>
 <%= render @products %>
<% end %>

動く。
速い。
シンプル。

だが、Product.all は静かにプロダクトを殺す。


なぜ危険か(Lighthouse の警告を無視するな)

❌ DOM ノード数の爆発

Lighthouse は **「1ページあたり DOM ノード数 1,500 未満」**を推奨している。
商品カード 1 件が 20〜30 ノードだとすると、100 件で簡単に上限を超える。

Turbo は Morphdom によって差分更新を行うが、
大量の DOM 比較と置換はブラウザのメインスレッドを確実に塞ぐ。

❌ ペイロードの肥大化

HTML は JSON より表現力が高いが、その分サイズも大きい。
数百〜数千件のレコードを HTML で一気に送れば、数 MB 単位の転送量になる。

👉 問題は「遅い」ことではない。
👉 負荷が線形ではなく、ある時点で急激に崩れることだ。


✅ 改善例:Frame は常に Bounded であるべき

ページネーションは必須だ。
そして 書き方を間違えないことも重要。

# Controller
@products = Product.page(params[:page]).per(20) # 常に上限を設ける 
<%= turbo_frame_tag "products" do %>
  <%= render @products %>
  <%= paginate @products %>
<% end %>

👉 Frame は「小さなページ」だと考えろ。


2️⃣ 「非 HTML フォーマットはどうする?」という分岐点

def index
  @products = Product.all
  respond_to do |format|
    format.html
    format.json { render json: @products }
  end
end

このコードを書いた瞬間、設計の地獄が始まる。

Hotwire(HTML over the Wire)と JSON API を
1 つのコントローラで混在させると、必ず歪む。

起きる問題

  1. 責務の混合
    • HTML 用の eager load
    • JSON 用の serializer / field 制御
      → 最適化方針が衝突する
  2. キャッシュ戦略の崩壊
    • ETag / Last-Modified の管理が複雑化
    • フォーマットごとに無駄な再生成が発生

👉 鉄則
Hotwire を選んだ Controller は HTML に特化させろ。
API が必要なら Api::V1::ProductsController を分けて作る。


3️⃣ リアルタイム更新という名の「自己 DDoS」

チュートリアルでよく見る
**「モデルのコールバックで broadcast」**は、最大級のアンチパターンだ。

❌ モデルへの副作用埋め込み

class Post < ApplicationRecord
  after_create_commit { broadcast_append_to "posts" }
end 

Rails らしく見えるが、大規模アプリでは凶器になる。

なぜ危険か


❌ 全ユーザーへの一斉送信

ActionCable.server.broadcast("global_feed", ...) 

1 人の投稿が、
10,000 人のオンラインユーザー全員に配信される。

👉 典型的な Thundering Herd 問題
👉 線形ではなく、一気に死ぬ


✅ 改善例:Service Object + 非同期 + 間引き

UI への副作用は モデルではなくユースケース層に置く。

# app/services/create_post_service.rb class
CreatePostService
  def call(params, user)
    post = Post.new(params.merge(user: user))
    if post.save      # UI 更新が必要な場合のみ、明示的に
      BroadcastNewPostJob.perform_later(post)
    end
    
    post
  end
end 

👉 教訓
Turbo Streams は「DB の鏡」ではない。
「UI への手紙」だ。送る相手とタイミングを選べ。


4️⃣ Fragment Caching × Turbo は「必須科目」

Hotwire アプリが遅くなる原因の多くは、
サーバーサイドの HTML レンダリング時間だ。

SPA はクライアント CPU を使う。
Hotwire は サーバー CPU を使う。

つまり、

キャッシュなしの Hotwire は、高負荷時に確実に詰まる。

<%= render partial: "posts/post",
           collection: @posts,
           cached: true %> 

📊 Real Data: パフォーマンスの現実

処理コスト感補足
単純な HTML render5〜20msERB + 文字列結合
render_to_string(partial)20〜50ms呼び出し回数で線形増加
JSON API render2〜10ms軽量 serializer 前提
Broadcast(1 user)+ 数十 msJob + Redis publish
Broadcast(10k users)数秒〜TimeoutAction Cable 側が詰まる

👉 broadcast × partial × 多人数 = 即アウト


結論

Hotwire は「速い」のではない。
「初期ロード体験が良い」だけだ。

サーバー負荷は、
設計を誤ると SPA より高くなる。

だからこそ重要なのは、

という、古典的だが本質的な Rails のチューニングだ。


🤔 命題

Hotwire は素晴らしい。
だが、それは **「正しく使えば」**の話だ。

何十万、何百万のユーザーが
Turbo Streams や Action Cable を通じて
リアルタイム更新を受け取る世界を想像してほしい。

そのたびに、

が発生する。

最適化されていなければ、
それは UX 改善ではなく、サーバーへの負債になる。

スケーラビリティとパフォーマンスを維持するために、
Turbo Streams の設計・キャッシュ戦略を含めた最適化は必須だ。

他にも実運用での知見があれば、ぜひコメントで共有してほしい。


Advertisement
#turbo#hotwire#streams#rails#ruby#on#action#cable#ベストプラクティス#パフォマンス#アキテクチャ
Advertisement