hypernova-rubyでもfragmenet cacheする

airbnb/hypernova-ruby
を普通に使ってfragment cacheをしようと思うと事故るのでその話を。

ちなみに現在の最新版v1.3.0での話です。

なぜ事故るのか

hypernova-rubyの使い方

contollerにaround_filterを追加して

class MyController < ApplicationController
  around_filter :hypernova_render_support
end

テンプレート内で

<%= render_react_component('MyComponent', props) %>

と使います。

実際何が起きているのか

  1. render_react_componentはSSRされたものと置換される文字列(トークン)を返す。トークンとコンポーネント名、propsの対応関係を持っておく。
  2. テンプレートのrender完了後に全てのコンポーネントのぶんをまとめてhypernova/serverにbatchリクエストを送る。
  3. 1からの対応関係に応じて、response.body内のトークンをSSRしたものに置換する。

という処理が行われます。

なので

<% cache 'MyComponent' do %>
  <%= render_react_component('MyComponent', props) %>
<% end %>

等としてしまうと置換前のトークンがキャッシュされて、キャッシュされる初回のみは正しく表示されるものの、
それ移行のリクエストではそのトークンに対応するものがbatchリクエストに含まれないので、トークンが置換されずにそのまま表示されるということが起きてしまいます。

どうするか

hypernova-rubyのREADMEにも書いてある低レベルAPIを使って、ブロックごとにhypernova/serverへのリクエストと置換までやってしまうhelperを書きます。

module ApplicationHelper
  def hypernova_batch_replace(&block)
    builder = HypernovaBatchBuilder.new(self, @_hypernova_service)
    str = capture(builder, &block)
    builder.replace(str)
  end
end

ここでHypernovaBatchBuilderは以下。

class HypernovaBatchBuilder
  def initialize(service)
    @batch = Hypernova::Batch.new(service)
    @token_uuid_map = {}
  end

  def render_react_component(name, **props)
    uuid = SecureRandom.uuid
    token = @batch.render(name: name, data: props)
    @token_uuid_map[token] = uuid
    uuid
  end

  def replace(str)
    result = @batch.submit!
    result.reduce(str) do |s, (token, v)|
      uuid = @token_uuid_map[token]
      s.sub(uuid, v)
    end.html_safe
  end
end

これをテンプレート内で

<% cache 'cache-key' do %>
  <%= hypernova_batch_replace do |b| %>
    <%= b.render_react_component('MyComponent', props) %>
  <% end %>
<% end %>

と使います。

HypernovaBatchBuilderに色々生やせば自分のユースケースにあったことがもっと色々出来ると思います。
例えば、クライアント側でrenderしない場合にはnokogiri等でdata関係の余計な要素を削るようなメソッドを生やすとか。

注意点としては、

あたりですかね。
自分の場合には平均レスポンス時間で~30msぐらいは削れました。