YAMLとERBでJSONを書けるハンドラを書いたものの遅くなってしまったので、一旦YAMLとERBのことは忘れ、evalとしては最速になるようにhashをJSONに変換するアプローチで再度書いてみました。
また前回同様、
- モンキーパッチをしない
- N+1 find_template_pathsを避ける
- Railsのcollectionのパーシャルレンダリングの仕組みを使う
できあがったものはこれ↓
インストールと使い方
インストールはgem 'jv'
でbundle installするだけ。
使い方は amatsuda/jb とほぼ同様の使い方になりますが、パーシャルレンダリングの方法が違います。
json = {}
json[:string_property] = @string
json[:integer_property] = 123
json[:array_property] = @integers
json[:object_property] = {
single_partial: r.(partial: 'api/record', locals: { record: { a: 123, b: 100 } }),
collection_partial: r.(partial: 'api/record', collection: @records, as: :record)
}
json
パーシャルレンダリングしたい場合は r.(partial: ‘xxxx’, ….)
を呼び出します。
内部のしくみ
Railsのcollectionのパーシャルレンダリングの仕組みを使ってしまうと、レンダリングされた個々のレコードのビューがjoinメソッドによって文字列化+連結されてしまいます。JSONの場合は配列を期待しているので、カンマ区切り連結しつつ先頭と末尾に[]
が付いてほしいのですが仕組み上できません。
なので、jvではハンドラー側でよしなに配列のJSON文字列にしています。
rはJv::Renderのインスタンスで、callメソッドで実際のパーシャルレンダリングを行っています。
module Jv
class Render
def initialize(context)
@context = context
end
def call(options)
render_collection = options[:collection].present?
options[:locals] ||= {}
options[:locals].merge!(r: self, _partial: true) if render_collection
json = @context.render options
if render_collection
json.prepend('[')
json[-1] = ']'
end
JSON.load(json)
end
end
end
collectionの引数が指定された場合は_partial: trueを指定してレンダリングします。_partialが定義されていると、hash.to_jsonの後ろにカンマを付けています。
require 'jv/view'
require 'jv/render'
module Jv
class Handler
cattr_accessor :default_format
self.default_format = Mime[:json]
def self.call(template)
<<~SRC
r ||= Jv::Render.new(self)
_partial ||= nil
hash = ->{ #{template.source} }.()
_partial.nil? ? hash.to_json : "\#{hash.to_json},"
SRC
end
end
end
このままjoinすると後ろに余計なカンマができるので削除しつつ、配列の括弧で囲んであげて最後にJSON.loadしてハッシュ化します。
if render_collection
json.prepend('[')
json[-1] = ']'
end
JSON.load(json)
配列結合が無理矢理感満載ですが一応動いているようです。
ベンチマーク
今回は本番環境でjbuilderより〜3倍程度速かったです!Development
* Rendering 10 partials via render_partial
Warming up --------------------------------------
jbuilder 8.000 i/100ms
jv 10.000 i/100ms
Calculating -------------------------------------
jbuilder 79.193 (± 7.6%) i/s - 400.000 in 5.085408s
jv 106.160 (± 6.6%) i/s - 530.000 in 5.013174s
Comparison:
jv: 106.2 i/s
jbuilder: 79.2 i/s - 1.34x slower
* Rendering 100 partials via render_partial
Warming up --------------------------------------
jbuilder 1.000 i/100ms
jv 7.000 i/100ms
Calculating -------------------------------------
jbuilder 14.927 (±13.4%) i/s - 74.000 in 5.029316s
jv 71.601 (± 7.0%) i/s - 357.000 in 5.019427s
Comparison:
jv: 71.6 i/s
jbuilder: 14.9 i/s - 4.80x slower
* Rendering 1000 partials via render_partial
Warming up --------------------------------------
jbuilder 1.000 i/100ms
jv 1.000 i/100ms
Calculating -------------------------------------
jbuilder 1.614 (± 0.0%) i/s - 9.000 in 5.587806s
jv 19.176 (±10.4%) i/s - 95.000 in 5.001148s
Comparison:
jv: 19.2 i/s
jbuilder: 1.6 i/s - 11.88x slower<code>
</code>
Production
* Rendering 10 partials via render_partial
Warming up --------------------------------------
jbuilder 48.000 i/100ms
jv 88.000 i/100ms
Calculating -------------------------------------
jbuilder 496.263 (± 8.7%) i/s - 2.496k in 5.075655s
jv 888.175 (± 7.0%) i/s - 4.488k in 5.080282s
Comparison:
jv: 888.2 i/s
jbuilder: 496.3 i/s - 1.79x slower
* Rendering 100 partials via render_partial
Warming up --------------------------------------
jbuilder 6.000 i/100ms
jv 18.000 i/100ms
Calculating -------------------------------------
jbuilder 64.780 (±10.8%) i/s - 324.000 in 5.069040s
jv 192.008 (± 6.2%) i/s - 972.000 in 5.082385s
Comparison:
jv: 192.0 i/s
jbuilder: 64.8 i/s - 2.96x slower
* Rendering 1000 partials via render_partial
Warming up --------------------------------------
jbuilder 1.000 i/100ms
jv 2.000 i/100ms
Calculating -------------------------------------
jbuilder 6.645 (±15.0%) i/s - 33.000 in 5.001550s
jv 23.705 (± 8.4%) i/s - 118.000 in 5.000942s
Comparison:
jv: 23.7 i/s
jbuilder: 6.6 i/s - 3.57x slower
1000件のパーシャルレンダリングでstackprofを取ってみたらこんな感じでした。
TOTAL (pct) SAMPLES (pct) FRAME
8 (13.1%) 8 (13.1%) ActiveSupport::ToJsonWithActiveSupportEncoder#to_json
16 (26.2%) 8 (13.1%) ActiveSupport::JSON::Encoding::JSONGemEncoder::EscapedString#to_json
11 (18.0%) 7 (11.5%) ActionView::PathResolver#find_template_paths
24 (39.3%) 7 (11.5%) Hash#as_json
30 (49.2%) 6 (9.8%) ActiveSupport::JSON::Encoding::JSONGemEncoder#jsonify
4 (6.6%) 4 (6.6%) ActionView::Template#instrument_payload
18 (29.5%) 2 (3.3%) #<Module:0x007faaf62d1d30>.generate
特に大きなボトルネックになっているところは無さそうですが、さすがにto_jsonしまくっているのでそこは少し目立ってます。