219
179

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

パーシャルをrenderする際のパフォーマンスに関する注意点

Last updated at Posted at 2015-09-20

ビュー内で部分テンプレート(パーシャル)を繰り返し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
呼び出し側.html.erb
    <%= 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特有の間口の広い引数対応が大変そうだったので、ハンカチを噛み締めながら撤退しました。

もっといい方法があれば教えて下さい。

参考

219
179
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
219
179

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?