YAMLとERBでJSONを書けるハンドラを書いたものの遅くなってしまったので、一旦YAMLとERBのことは忘れ、evalとしては最速になるようにhashをJSONに変換するアプローチで再度書いてみました。
また前回同様、
- モンキーパッチをしない
- N+1 find_template_pathsを避ける
- Railsのcollectionのパーシャルレンダリングの仕組みを使う
というのをコンセプトでやっています。
できあがったものはこれ↓
インストールと使い方
インストールは
1 |
gem 'jv' |
でbundle installするだけ。
使い方は amatsuda/jb とほぼ同様の使い方になりますが、パーシャルレンダリングの方法が違います。
1 2 3 4 5 6 7 8 9 |
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メソッドで実際のパーシャルレンダリングを行っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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の後ろにカンマを付けています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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してハッシュ化します。
1 2 3 4 5 |
if render_collection json.prepend('[') json[-1] = ']' end JSON.load(json) |
配列結合が無理矢理感満載ですが一応動いているようです。
ベンチマーク
今回は本番環境でjbuilderより〜3倍程度速かったです!
Development
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
* 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> |
Production
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
* 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を取ってみたらこんな感じでした。
1 2 3 4 5 6 7 8 |
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しまくっているのでそこは少し目立ってます。
コメントを残す