Qiitaにも書いたのですが、ブログの方にも書いておこうと思います。
ビュー内で部分テンプレート(パーシャル)を繰り返し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特有の間口の広い引数対応が大変そうだったので、ハンカチを噛み締めながら撤退しました。
もっといい方法があれば教えて下さい。
関連する記事
- Ruby on Rails + Bootstrap3 + simple_form チュートリアル
- Railsアプリを『浅く』パフォーマンス・チューニングしてみる(その2)
- Rails+JSフレームワークでリアルタイム掲示板を作成してみる(Backbone.js編)
- Rails + simple_form + Foundation 5 チュートリアル
- RailsでActiveRecordモデルのコレクションに対してメソッドを追加する