Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第30章 パフォーマンスと N+1

この章のねらい

Hotwire は、画面を部分更新するので、体感が軽くなります。しかし、それは「サーバーが速い」という意味ではありません。サーバー側が遅ければ、部分更新であっても遅いままです。

この章では、Hotwire の体感速度を損なう Rails 側の問題を見つけて直します。とくに N+1 クエリは、Relay のような関連の多いアプリで起きやすい問題です。

30.1 Hotwire は遅い Rails を隠さない

第8部の軸を、ここで思い出します。「Hotwire は、遅い Rails も危ない Rails も隠してくれない」。

部分更新は、転送する HTML を小さくします。しかし、その HTML を作るのは Rails です。一覧を描くのに大量のクエリが走っていれば、frame でも stream でも、生成は遅いままです。Hotwire を入れたから速くなった、と安心せず、サーバー側の生成コストを測ります。

30.2 partial rendering のコスト

Relay の一覧は、_task partial をタスクの数だけ描きます。partial を 1 件ずつ描くのには、それなりのコストがあります。

Rails のコレクション描画(render @tasks)は、同じ partial を繰り返すときに最適化が効くので、1 件ずつ render を呼ぶより効率的です。一覧は、できるだけコレクション描画でまとめて描きます。それでも重いときは、30.5 のキャッシュを検討します。

30.3 N+1 と preload

一覧描画で N+1 が起きる様子と、includes で解消する様子をクエリ本数で対比する図。

Relay のタスクは、担当者(assignee)・タグ(tags)・コメント(comments)を持ちます。一覧で各タスクの担当者名やタグを表示すると、タスク 1 件ごとに関連を引くクエリが走ります。これが N+1 です。

たとえば、20 件のタスクを表示するのに、担当者を引くクエリが 20 回、タグを引くクエリが 20 回……と積み重なります。一覧の表示が、急に遅くなります。

直し方は、関連を先読み(preload)することです。includes を使います。

app/controllers/tasks_controller.rbindex

@tasks = Task.includes(:assignee, :tags).order(:id)

includes(:assignee, :tags) を付けると、担当者とタグをまとめて読み込み、N+1 が解消します。一覧で使う関連を、includes で先読みします。

コメントは少し事情が違います。一覧に出したいのが「コメント件数」だけなら、includes(:comments) でコメント本体を全部読み込むのは無駄です。件数用途では、counter_cache(件数をカラムに持たせる仕組み)を使うのが効率的です。一覧で本文まで使うなら先読みする、件数だけなら counter_cache、と用途で使い分けます。

ここで大事なのは、第18章で触れた点です。broadcast の partial でも N+1 は起きます。配信のたびに関連を引けば、配信が遅くなります。配信に使う partial でも、必要な関連を先読みします。

30.4 broadcast の回数

リアルタイム更新(第18章)は、配信の回数にも気を配ります。

broadcasts_to は、レコードの保存ごとに配信します。たくさんのレコードを次々に保存する処理では、そのたびに配信が走り、サーバーにもクライアントにも負荷がかかります。

  • 重い配信は、非同期版(broadcast_*_later_to)でジョブに逃がします(第18章)。
  • 大量の更新を一度にするなら、1 件ずつの配信ではなく、「最新に揃えて」と伝える broadcast refresh(第9章・第15章)を検討します。

30.5 キャッシュの使いどころ

描画そのものを減らすには、フラグメントキャッシュが効きます。partial の描画結果をキャッシュし、変わっていなければ作り直しません。

<% cache task do %>
  <%= render task %>
<% end %>

cache task は、task のキャッシュキー(更新時刻を含む)でキャッシュします。タスクが変わればキーが変わり、自動でキャッシュが切り替わります。一覧と部分更新で同じ partial を共通化してあれば(第12章・第17章)、どちらの描画でも同じキャッシュが効きます。partial の共通化は、保守だけでなく、キャッシュの面でも利きます。

30.6 大きすぎる Turbo Streams

Turbo Streams は、1 レスポンスに複数の命令を入れられます(第15章・第17章)。便利ですが、入れすぎると重くなります。

たとえば、一度に何百行も append する命令を返すと、サーバーの描画も、ブラウザの適用も重くなります。大量のデータは、ページネーション(第24章)で分けて送ります。1 レスポンスの命令は、必要な分にとどめます。

30.7 測定してから直す

最後に、いちばん大事な原則です。推測で直さず、測ってから直します。

  • サーバーログを見れば、1 リクエストで走ったクエリの数と時間が分かります。N+1 は、同じようなクエリが並ぶので、ログで見つけられます。
  • N+1 を自動で検出する gem(Bullet など)を開発環境に入れると、見落としを減らせます。
  • どこが遅いかは、プロファイラ(rack-mini-profiler など)で測ります。

「ここが遅いはず」という思い込みで includes を撒くと、かえって無駄な読み込みが増えることもあります。測って、遅い箇所を特定してから直す。これが順序です。

第30章では、Hotwire の体感を支える Rails 側の性能を扱いました。次の第31章では、部分更新や broadcast でも認可を崩さないための、認証・認可・セキュリティを学び、第8部を締めます。

参考資料