Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Railsのロシア人形キャッシング(Russian Doll Caching)徹底解説:ネストされたフラグメントキャッシュでビューを高速化

Railsの組み込みフラグメントキャッシュをさらに一歩進めてパフォーマンスを最大化したいなら、「ロシア人形キャッシング(Russian Doll Caching)」という手法が強力な武器になります。本記事では、この手法の仕組みと実装方法を詳しく解説します。

フラグメントキャッシュとは

Railsに標準搭載されているフラグメントキャッシュでは、レンダリング済みビューの一部が「ビューフラグメント」として保存され、再度リクエストされた際に再利用されます。これらのキャッシュ済みフラグメントは「stale(陳腐化)」状態になるまで使い回されます。staleとは、フラグメント作成後に表示対象のデータが変更され、内容が古くなってしまった状態を指します。フラグメントキャッシュについてより深く学びたい方は、以前の記事もあわせてご覧ください。

👋 キャッシュにとどまらず、パフォーマンス全般について学びたい方は、Rubyパフォーマンス監視チェックリストもぜひチェックしてみてください。

フラグメントキャッシュだけでも、特に複雑なビューや多数のパーシャルを含むビューにおいて大きな速度向上が期待できます。しかし、そこからさらに改善できるのが「ロシア人形キャッシング」です。

この手法では、ビューフラグメントを入れ子状に配置していきます。名前の由来となったマトリョーシカ人形のように、キャッシュを小さな断片へ分割することで、ネストされたフラグメントのうち1つだけが変更された場合でも、外側のキャッシュを素早く再構築できるようになります。

ロシア人形キャッシングの実例

例として、商品を販売するストアを想定しましょう。各商品には複数のバリアント(バリエーション)を持たせることができ、たとえば同じ商品を複数の色で販売できます。商品一覧ページでは、販売中の商品とそのすべてのバリアントを表示します。

商品一覧ページでは、各商品のパーシャルをcacheブロックで囲んでいます。キャッシュキーはproductオブジェクトから生成され、これを使ってキャッシュフラグメントを無効化します。キーはオブジェクトのid、updated_atの日付、テンプレートツリーのダイジェストで構成されているため、オブジェクトが変更された場合やテンプレートの内容が変わった場合には、自動的にstaleと判定されます。

# app/views/products/index.html.erb
<h1>Products</h1>
 
<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

ヒント: わかりやすさのためコード全体を書き出していますが、次のように書くことで、各商品をキャッシュブロック付きでまとめてレンダリングすることも可能です。<%= render partial: 'products/product', collection: @products, cached: true %>

商品のパーシャルでは、商品の各バリアントを行としてレンダリングします。

# app/views/products/_product.html.erb
<article>
  <h1><%= product.title %></h1>
 
  <ul>
    <% product.variants.each do |variant| %>
      <%= render variant %>
    <% end %>
  </ul>
</article>

キャッシュの無効化

Railsのフラグメントキャッシュにおけるキャッシュキーの仕組みは、無効化処理をかなり楽にしてくれます。それでも、キャッシュ検証の問題から完全に解放されるわけではありません(コンピュータサイエンスで有名な「難しいこと2つ」の1つですね)。

この例では、商品のバリアント一覧を含む商品パーシャルをキャッシュしています。しかし、キャッシュキーにはバリアントに関する情報が一切含まれていないため、新しいバリアントを追加しても、商品自体が変更されない限り表示には反映されません。

この問題を解決するには、バリアントに何らかの変更があったときに、必ず親である商品側も変更されるようにします。具体的には、そのタイミングで商品のupdated_at属性を更新します。これは非常によくあるケースなので、belongs_to(およびActiveModelの他の関連メソッド)には:touchというオプションが用意されており、親オブジェクトのupdated_atを自動的に更新してくれます。

class Variant < ApplicationRecord
  belongs_to :product, touch: true
end

ネストされたフラグメント

バリアントが変更されたときに商品フラグメントも更新されるようになったので、今度はバリアント自体もキャッシュしてみましょう。先ほどと同じ要領で、それぞれをcacheブロックで囲みます。

<article>
  <h1><%= product.title %></h1>
 
  <ul>
    <% product.variants.each do |variant| %>
      <% cache(variant) do %>
        <%= render variant %>
      <% end %>
    <% end %>
  </ul>
</article>

ヒント: こちらも同様に、次のように書けば各バリアントをキャッシュブロック付きでまとめてレンダリングできます。<%= render partial: 'variants/variant', collection: product.variants, cached: true %>

コールドキャッシュの状態(rake tmp:cache:clearでキャッシュをクリアできます)では、最初のリクエストで各商品パーシャルがレンダリングされます。

ページをリクエストすると(開発環境でキャッシュを有効化するにはrails dev:cacheを実行することをお忘れなく)、各商品パーシャルがキャッシュされ、2回目のリクエストではキャッシュ済みのフラグメントが返されます。

Started GET "/products" for 127.0.0.1 at 2018-03-30 14:51:38 +0200
Processing by ProductsController#index as HTML
  Rendering products/index.html.erb within layouts/application
  Product Load (0.2ms)  SELECT "products".* FROM "products"
  Variant Load (0.9ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51)
  Rendered variants/_variant.html.erb (0.5ms)
  Rendered variants/_variant.html.erb (0.1ms)
  ...
  Rendered products/_product.html.erb (44.8ms) [cache miss]
  ...
  Rendered products/_product.html.erb (46.2ms) [cache miss]
  Rendered products/index.html.erb within layouts/application (1378.6ms)
Completed 200 OK in 1414ms (Views: 1410.5ms | ActiveRecord: 1.1ms)

Started GET "/products" for 127.0.0.1 at 2018-03-30 14:51:41 +0200
Processing by ProductsController#index as HTML
  Rendering products/index.html.erb within layouts/application
  Product Load (0.3ms)  SELECT "products".* FROM "products"
  Variant Load (12.7ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51)
  Rendered products/index.html.erb within layouts/application (48.1ms)
Completed 200 OK in 76ms (Views: 59.0ms | ActiveRecord: 13.0ms)

さあ、ロシア人形キャッシングの魔法を見てみましょう

ロシア人形キャッシングの真価が発揮されるのは、バリアントの1つを変更したときです。変更後に一覧ページへ再度アクセスすると、商品のupdated_at属性が変わったため、キャッシュされた商品フラグメントは再レンダリングされます。

商品パーシャルには商品のすべてのバリアントが含まれています。変更したバリアントのキャッシュフラグメントはstaleになっているため再生成が必要ですが、その他のバリアントは変更されていないため、キャッシュ済みフラグメントがそのまま再利用されます。ログを見ると、バリアントと商品のパーシャルがそれぞれ1回だけレンダリングされていることがわかります。

Started GET "/products" for 127.0.0.1 at 2018-03-30 14:52:04 +0200
Processing by ProductsController#index as HTML
  Rendering products/index.html.erb within layouts/application
  Product Load (0.3ms)  SELECT "products".* FROM "products"
  Variant Load (1.2ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51)
  Rendered variants/_variant.html.erb (0.5ms)
  Rendered products/_product.html.erb (13.3ms) [cache miss]
  Rendered products/index.html.erb within layouts/application (45.9ms)
Completed 200 OK in 78ms (Views: 73.5ms | ActiveRecord: 1.5ms)

最終的な結果

このようにキャッシュフラグメントを入れ子にすることで、キャッシュが完全に空の場合を除き、ビュー全体が最初からレンダリングされることはほぼなくなります。データが変更された場合でも、ページの大部分はキャッシュから直接配信されるのです。

本記事が、アプリケーションのパフォーマンスに対する新たな気づきにつながれば幸いです。それこそが私たちの使命だからです。キャッシュに関する記事を楽しんでいただけたなら、ActiveRecordのCounter Cache、RailsのCache Stores、コレクションキャッシング、そして冒頭で紹介したフラグメントキャッシングに関する記事など、さらなる「おかず」もぜひ味わってみてください。

  1. AWS LambdaでRailsアプリを動かす方法!Lambyを使ったデプロイ手順を徹底解説

    サーバーレスコンピューティングは、サーバーの管理やプロビジョニングの作業をクラウドプロバイダーにオフロードできる仕組みとして、多くのテックチームで急速に標準になりつつあります。その代表格がAWS Lambdaです。LambdaはNode.js、Java、Python、Rubyといった主要なプログラミング言語を幅広くサポートしています。しかし、言語そのものだけでなく、それらの言語で構築されたフレームワークの機能に依存するサーバーレス関数を実行したいケースもあるでしょう。 本記事では、RailsアプリケーションをAWS Lambda上で実行する方法を解説します。サーバーレスコンピューティングとAW

  2. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた