かなり有名な話かと思うが、 jbuilderのpartialは遅い。
ActionViewの機能の render partial: '...'
を内部的に呼び出しており、それが遅いからである。
そのため、数の多いレコードをjbuilderでレンダリングすると、だいたい数百msかかる。
# 内部でレコードごとにrender呼び出しているのが原因で遅い
json.posts('posts/post', collection: @posts, as: :post)
これを解決するために、 partial!
を呼び出さずに直接埋め込むこともある。
しかし、せっかく構造化して切り分けられたテンプレートを埋め込むと、見通しが悪くなり残念である。
json.posts(@posts) do |post|
json.call(post, ...)
end
というわけで、テンプレートを分割したまま高速化することを考えてみる。
効果の高そうな順に並べてみた。
1. partialのログをオフにする
デフォルトでは、Railsはレンダリングしたテンプレートの情報をログに書き出す。
下記のようなログは、見覚えがあるのではないだろうか?
INFO -- : Rendered api/posts/_post.json.jbuilder (8.0ms)
INFO -- : Rendered api/comments/_comment.json.jbuilder (0.2ms)
INFO -- : Rendered api/comments/_comment.json.jbuilder (0.8ms)
INFO -- : Rendered api/comments/_comment.json.jbuilder (0.9ms)
INFO -- : Rendered api/comments/_comment.json.jbuilder (0.3ms)
INFO -- : Rendered api/comments/_comment.json.jbuilder (1.0ms)
...
通常は軽微なものなのだが、数が多いとパフォーマンスに影響が出る。
なので、jbuilderの場合はログの出力を抑えるのを試みる。
# config/initializers/optimized_jbuilder_partial_rendering.rb
module OptimizeJBuilderPartialRendering
def render_partial(event)
# jbuilderの場合はログを書き出さない
super unless event.payload[:identifier].end_with?('.jbuilder')
end
end
ActiveSupport.on_load(:action_view) do
ActionView::LogSubscriber.prepend(OptimizeJBuilderPartialRendering) if Rails.env.production?
end
これで、検証環境では倍ぐらい速くなった。
2. 探索の正規表現を簡素にする
テンプレートの探索は、複数のパス(prefix + view_path)の Dir[Regexp]
を回して行われる。
端的に言えば、 Regexpのコスト * 探索回数
がテンプレート探索のコストである。
そのため、Regexpの内容を改良することで、テンプレートの探索速度が速くなる可能性がある。
ちなみにRegexpの中身はこんな感じで、locale、format、handlersなどが考慮されている。
%r!app/views/api/posts/_post{.ja,}{.html,.json,}{}{.erb,.builder,.raw,.ruby,.slim,.coffee,.haml,.faml,.jbuilder,}!
#details_for_lookup
を上書きして、自分にとって不要な要素を削ることで高速化を試みる。
class Api::PostsController < Api::ApplicationController
...
private
# 実はjbuilderはhandlersのみ最適化しているので、handlersはなくても良い。
# ただし、初回にcontrollerがrenderを呼び出す際には効果がある。
def details_for_lookup
{
locale: [],
format: [:json],
handlers: [:jbuilder]
}
end
end
巨大なアプリケーションでは、HTMLの探索が10倍ぐらい速くなったことがあるのだが、今回は効果がなかった。
検証環境は小さいアプリケーションなのと、jbuilderの最適化、ActionViewの探索パスのキャッシュが既に効いているからだと思う。
ちなみに、自分のアプリケーションを変更したい時は YourController.new.lookup_context.instance_variable_get(:@details)
でRegexpの大体の予想ができる。
今後も検証することがあれば追記していく。
ベンチマークは、APIアプリケーションを作って試しているだけなので、ぜひフィードバックください。