概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Partial rendering performance in Rails
- 原文公開日: 2017/11/17
- 著者: Alessandro Rodi
Rails: パーシャルとcollection:
でN+1を回避してビューを高速化(翻訳)
Railsでビューのレンダリング(特にパーシャル)を正しく行うことの重要性に気づいてない人をよく見かけます。本記事では、さまざまなアプローチのパフォーマンスの相対数値を比較します。このトピックは、多くのブログ記事で見落とされがちです。
N+1クエリの回避
最初の重要なトピックは「N+1クエリ」です。N+1クエリをのさばらせると速度低下が不可避になり、その他のパフォーマンス最適化も効かなくなってしまうことがあるため、ぜひとも回避しましょう!
非常にシンプルな例から見てみましょう。
<% @users.each do |*user*| %> <div class="post">
<%= *user*.post.title %> </div>
<% end %>
users
のリストを反復して、各user
をpost
としています。簡単ですね。
それでは、ユーザー1000人を以下のコードでassign
してテストしてみましょう。
assign(:users, *User*.all)
結果は以下のとおりです。
Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
with N+1 1.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
with N+1 0.291 (± 0.0%) i/s --- 2.000 in 7.158333s
1秒あたりでレンダリング可能なビュー数は0.291です。これはかなり残念な数値なので、ビューで発生しているN+1クエリを最初に解決しましょう。現在のビューでは、イテレーションのたびにDBでSELECT
を実行してuser
のpost
を取り出しています。
N+1を今後も解決するためにBullet gemを導入します。私はBulletが大好きです。次の3つの理由からGoldiloaderよりもBulletが好みです。
- Bulletのコード自動更新は変更してよいかどうかをプロンプトで確認してくれる。
- Bulletはproduction環境では実行できないようになっている。
- BulletはN+1クエリを隠蔽せず、検出と理解を支援してくれる。
GemfileにBulletを追加してtest環境向けに設定します。これによって、N+1クエリで例外がraiseされ、解決しないとテストを完了できないようになります。
# app/config/environments/test.rb
config.after_initialize do
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.raise = true
end
テストを再実行すると、必要な情報をBulletが表示してテストを失敗させます。
Bullet::Notification::UnoptimizedQueryError:
USE eager loading detected
User => [:post]
Add to your finder: :includes => [:post]
そしてassign
を以下のように変更します。
assign(:users, User.includes(:post))
修正後の結果は7倍ほど高速になりました。
Warming up --------------------------------------
with N+1 1.000 i/100ms
without N+1 1.000 i/100ms
Calculating -------------------------------------
with N+1 1.539 (± 0.0%) i/s - 8.000 in 5.305367s
without N+1 10.479 (± 9.5%) i/s - 52.000 in 5.057764s
Comparison:
without N+1: 10.5 i/s
with N+1: 1.5 i/s - 6.81x slower
パーシャルのレンダリング
それでは本記事の本題であるパーシャルの利用に進みましょう。個別のpost
をパーシャルに切り出してコードをリファクタリングすることにします。これはよい方法ですが、次のような駄目リファクタリングがあることも知っておきましょう。
1. パーシャルを切り出す。
<div class="post">
<%= user.post.title %>
</div>
2. レンダリングする。
<% @users.each do |user| %>
<%= render 'erb_partials/post', user: user %>
<% end %>
パフォーマンスは以下のようになります。
Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
inline 1.000 i/100ms
partial 1.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
inline 11.776 (± 8.5%) i/s --- 59.000 in 5.085002s
partial 5.648 (±17.7%) i/s --- 28.000 in 5.043322s
Comparison:
inline: 11.8 i/s
partial: 5.6 i/s --- 2.09x slower
なるほど、確かにコードが2倍も遅くなってしまいました。これはパーシャルのレンダリングを反復しているのが原因です。Railsはuser
ごとにパーシャルを「オープン」して評価しなければならなくなります。これを解決するにはcollection
を使います。
ビューを次のように変更します。
<%= *render* partial: 'erb_partials/post', collection: @users, as: :user %>
変更後の結果は以下のとおりです。
Warming up --------------------------------------
inline 9.000 i/100ms
partial 1.000 i/100ms
collection 6.000 i/100ms
Calculating -------------------------------------
inline 96.394 (±11.4%) i/s - 477.000 in 5.016304s
partial 8.989 (±22.2%) i/s - 43.000 in 5.108843s
collection 57.828 (±13.8%) i/s - 288.000 in 5.092763s
Comparison:
inline: 96.4 i/s
collection: 57.8 i/s - 1.67x slower
partial: 9.0 i/s - 10.72x slower
パフォーマンスを落とさずにコードをパーシャルに切り出せたことがわかります。修正後のコードでは、Railsによるパーシャルの評価は1回で完了し、その後user
ごとにレンダリングを行います。これは、先のN+1クエリで行った修正と同じに考えることができます。すなわち、この修正によってもコードをスケール可能にできるのです。
追伸
実際には(本記事のように)同一ページに1000ユーザーを表示するのはやめ、ページネーションを実装しましょう。
本記事で用いたコードはGitHubの私のリポジトリでご覧いただけます。
ボーナス: 他のレンダリングエンジンを試してみる
先ほどはerb
で比較しましたが、slim
やhaml
ではどうなのでしょうか?以下は比較の結果です。
Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
erb inline 8.000 i/100ms
erb partial 1.000 i/100ms
erb collection 5.000 i/100ms
slim inline 10.000 i/100ms
slim partial 1.000 i/100ms
slim collection 6.000 i/100ms
haml inline 9.000 i/100ms
haml partial 1.000 i/100ms
haml collection 4.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
erb inline 97.943 (±10.2%) i/s --- 488.000 in 5.046691s
erb partial 9.438 (±21.2%) i/s --- 46.000 in 5.026193s
erb collection 67.090 (± 6.0%) i/s --- 335.000 in 5.015540s
slim inline 104.373 (± 8.6%) i/s --- 530.000 in 5.122621s
slim partial 9.836 (±20.3%) i/s --- 49.000 in 5.123851s
slim collection 69.146 (± 7.2%) i/s --- 348.000 in 5.059607s
haml inline 85.732 (±11.7%) i/s --- 432.000 in 5.111380s
haml partial 8.180 (±24.4%) i/s --- 40.000 in 5.165770s
haml collection 41.069 (±21.9%) i/s --- 196.000 in 5.084682s
Comparison:
slim inline: 104.4 i/s
erb inline: 97.9 i/s --- same-ish: difference falls within error
haml inline: 85.7 i/s --- same-ish: difference falls within error
slim collection: 69.1 i/s --- 1.51x slower
erb collection: 67.1 i/s --- 1.56x slower
haml collection: 41.1 i/s --- 2.54x slower
slim partial: 9.8 i/s --- 10.61x slower
erb partial: 9.4 i/s --- 11.06x slower
haml partial: 8.2 i/s --- 12.76x slower
いずれの場合も、インラインのレンダリングが最もよい結果を残しています。slim
のパフォーマンスはerb
と同程度(またはわずかに上回る)で、haml
のレンダリング時間はパーシャルでやや落ちるようです。
結論
- パーシャルを使いましょう。ためらう必要はありません。
- インラインレンダリングは確かに高速ですが、コードのメンテナンス性/読みやすさ/テストのしやすさも重要です。
- すなわち、必要に応じてビューをパーシャルに分割しましょう。
- ただし、
collection
を使って正しく行いましょう。 - もちろんN+1クエリは避けましょう。