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


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特有の間口の広い引数対応が大変そうだったので、ハンカチを噛み締めながら撤退しました。

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

関連する記事


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください