ビュー内で部分テンプレート(パーシャル)を繰り返しrenderする場合、ちょっと気をつけておかねばならないことがあります。自戒の念を込めて、Qiitaに初投稿してみたいと思います。
検証した環境
- Rails 4.2 (3.2系でも同様だったはず)
コレクションを繰り返しrenderする場合
可能な限りrenderメソッドにはコレクションを渡しましょう。each等でぐるぐる回してrenderしても表示結果は変わりませんが、パフォーマンスは悪いです。
# × 悪い例
<% @posts.each do |post| %>
<%= render post %>
# もちろん、↓こう書いても
<%= render 'posts/post', post: post %>
# ↓こう書いてもダメ
<%= render partial: 'posts/post', locals: { post: post } %>
<% end %>
# ◯ 良い例
<%= render @posts %>
# 冗長だが、↓これでも良い
<%= render partial: 'posts/post', collection: @posts %>
なんとなくボーッとコーディングしてると、繰り返し?→じゃあeachじゃん、と条件反射的に書いちゃってる場合があります。どれぐらいパフォーマンスに差が出るのか、1,000件のpostsをそれぞれの方法でrenderしてみました。
development | production | |
---|---|---|
悪い例 | 2,500 ms | 250 ms |
良い例 | 150 ms | 70 ms |
1,000件もパーシャルをrenderすることはそうそう無いですし、production環境だと劇的に遅くなるわけでもありません。しかし、development環境では差が結構出ますし、パフォーマンスは少しでも速いに越したことはありませんし、何よりrailsが用意してくれているベストな方法をチョイスしないのは罪です。コレクションを渡しましょう。
上記例のようにコレクションの中身がActiveRecordモデルじゃない場合も、わざわざコレクションにして渡してあげた方がいいと思います。
# ↓こうではなくて
<% 1_000.times do |i| %>
<%= render 'counter', { now: Time.zone.now, index: i } %>
<% end %>
# ↓この方が速い
<%= render partial: 'counter', collection: (1..1_000).to_a, as: :index, locals: { now: Time.zone.now } %>
ちなみに、なぜパフォーマンスに差が出るのか少し調査してみたのですが、主にはパーシャルファイルを探して開く部分にあるようです(パーシャルファイルは1つなので、本来は1度しか実行しなくてよいところを、eachの場合は1,000回実行することになる)。developmentが遅いのはデバッグ情報の取得等々かな。
コレクションを渡せない場合(fields_forとか)
例えば、Postがbelongs_to :blog
となっており、Blogが以下のようになっている場合、
has_many :posts
accepts_nested_attributes_for :posts
ビューではfields_for
メソッドを使用することができます。この時、fields_forで描画する内容をパーシャルにしたいことがあると思います。
<%= form_for @blog do |f| %>
...
<%= f.fields_for :posts do |builder| %>
<%= render 'posts/form', post: builder.object, f: builder %>
<% end %>
...
<% end %>
この場合は先程のケースのようにrenderにコレクションを渡すことができません。考えられる対応方法は・・・
- postsの件数(=fields_forでループされる件数)が大したことが無いのであれば気にしない
- バーシャルにしない
といった感じでしょうか。まあ、諦めろっちゅうことです。
そんな消極的なのは嫌だ!という人は、def_erb_methodを使用して、パーシャルビューの描画をerbメソッド化してしまうことができます。
module PostsHelper
extend ERB::DefMethod
def_erb_method 'render_post_form(post, f)', "#{Rails.root}/app/views/posts/_post.html.erb"
end
<%= f.fields_for :posts do |builder| %>
<%= render_post_form(builder.object, builder).html_safe %>
<% end %>
これでパーシャルを使用しない場合と同程度に高速化されます。ただし上記の実装だと、開発中にposts/_post.html.erb
の内容を変更しても、サーバを再起動するまでその変更が反映されません。
もう少し汎用的かつサーバ再起動を不要とするために、以下のように実装してみました。erbメソッドを描画中のビューコンテキスト(=無名クラスのインスタンス)の特異クラスに定義するようにします。
module ApplicationHelper
def render_here(path_to_partial, locals = {})
method_name = "render_here_#{path_to_partial.gsub(%r{[/\.]}, '__')}"
unless respond_to?(method_name)
class_eval do # =singleton_class.class_eval(ActiveSupport拡張)
extend ERB::DefMethod
def_erb_method("#{method_name}(#{locals.keys.join(',')})", path_to_partial)
end
end
send(method_name, *locals.values).html_safe
end
end
<%= f.fields_for :posts do |builder| %>
<%= render_here "#{Rails.root}/app/views/posts/_post.html.erb", post: builder.object, f: builder %>
<% end %>
簡素な実装ですが、普段使いには問題なく動いています。railsをハックして高速renderをgem化してやろうかと思いましたが、rails特有の間口の広い引数対応が大変そうだったので、ハンカチを噛み締めながら撤退しました。
もっといい方法があれば教えて下さい。