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

JavaScriptスプリンクルでRailsアプリケーションを高速化する方法

Railsのフラグメントキャッシュは、ページのより多くの部分がキャッシュされるほど大きな速度向上をもたらします。しかし、動的なコンテンツやユーザー固有のコンテンツが多いページでは、その恩恵を受けにくくなります。そこで有効なのが「JavaScriptスプリンクル」という手法です。オランダのパンにふりかけるチョコ菓子「ハーゲルスラッハ」のように、ページ全体にさっとJavaScriptを振りかけるイメージで、ページの残りの部分はキャッシュから直接配信し、ユーザー固有のコンテンツだけを追加リクエストで後から読み込みます。

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

Railsアプリケーションでは、レンダリングされたページの断片(フラグメント)をキャッシュすることでレスポンスを高速化できます。スマートなキャッシュキーを使えば、ビューに表示されるデータが更新されたタイミングでフラグメントが自動的に無効化されるため、古い内容が表示される心配はありません。

キャッシュされたフラグメントは、利用頻度が高いほど少ないキャッシュ容量で大きな速度向上を実現できます。しかし、ページの内容が現在ログインしているユーザーに依存する場合、フラグメントのキャッシュは一気に難しくなります。

課題:未読レスポンスの表示

例として、Railsで構築されたブログを考えてみましょう。このブログでは、各記事に複数のレスポンス(コメント)を付けられます。認証システムを導入してユーザーが自分のアカウントでサインインしてレスポンスを投稿できるようにし、各記事ページにはレスポンスの一覧を表示します。ユーザーの利便性を高めるため、新しいレスポンスにはアイコンと異なる背景色で「未読」マークを付けることにしました。

ここで各記事をcacheブロックで囲むと、誤ったレスポンスが未読として表示される危険があります。あるユーザーが記事ページをリクエストするとレスポンスがキャッシュされ、その後別のユーザーが同じページをリクエストすると、最初のユーザーの未読情報を含んだキャッシュ済みフラグメントがそのまま返されてしまうのです。

# app/views/articles/show.html.erb
<%= cache(@article) do %>
  <h1><%= @article.title %></h1>
 
  <%= simple_format(@article.content) %>
 
  <section id="responses">
    <h2>Responses</h2>
 
    <% @article.responses.each do |response| %>
      <div class="<%= response.read_by?(@current_user) ? 'read' : 'unread' %>">
        <%= time_tag(response.created_at) %>
        <strong><%= response.name %></strong>: <%= response.content %>
      </div>
    <% end %>
  </section>
<% end %>

これを解決する方法のひとつは、cacheヘルパーメソッドに渡す引数として、@articleだけでなく[@article, @current_user]のように現在ログイン中のユーザーをキャッシュキーに含めることです。

# app/views/articles/show.html.erb
<%= cache([@article, @current_user]) do %>
  <h1><%= @article.title %></h1>
 
  # ...
<% end %>

これにより各ユーザーは自分自身の未読レスポンスを見られるようになりますが、記事のフラグメントがユーザーごとに個別にキャッシュされるため、キャッシュによる速度向上の効果はほとんど失われてしまいます。

JavaScriptスプリンクルによる解決

ページの大部分はすべてのユーザーにとって共通なので、キャッシュ済みの記事フラグメントを全訪問者で再利用したいところです。そのために、記事をキャッシュから読み込んだうえで、ページのロード完了後にJavaScriptのリクエストでユーザー固有のコンテンツを取得する方法があります。JavaScriptの機能を少し振りかけるだけで、次のようなメリットが得られます。

  1. ページを一度キャッシュすれば、未認証のゲストにも他のユーザーにも再利用でき、ユーザーごとにフラグメントを個別にキャッシュする必要がなくなります。
  2. 最も重要なコンテンツを先に読み込んで最速のレスポンスタイムを実現し、未読数などの二次的な機能は後から読み込めます。
  3. 追加リクエストがJavaScript経由なので、ページ全体をCDNでエッジキャッシュでき、パフォーマンスをさらに向上させられます。

クリーンアップ:動的要素の取り除き

まず、ページから動的なコンテンツを取り除いて、キャッシュしやすい状態にします。cacheブロックのキャッシュキーから@current_userを外し、ユーザーごとのキャッシュをやめます。続いて、コントローラーから未読数を検索するクエリを削除し、ビューからCSSクラス名を取り除きます。

# app/views/articles/show.html.erb
<%= cache(@article) do %>
  <h1><%= @article.title %></h1>
 
  <%= simple_format(@article.content) %>
 
  <section id="responses">
    <h2>Responses</h2>
 
    <% @article.responses.each do |response| %>
      <div data-response-id="<%= response.id %>">
        <%= time_tag(response.updated_at) %>
        <strong><%= response.name %></strong>: <%= response.content %>
      </div>
    <% end %>
  </section>
<% end %>

これで汎用的なページになり、キャッシュしやすくなりましたが、未読レスポンス機能が失われています。次はこれをJavaScriptで復活させましょう。

JSON APIエンドポイントの作成

まず、ユーザーの未読レスポンスを取得するエンドポイントを作成します。すでにレンダリング済みのページの状態を後から変更したいので、JSON形式でデータをリクエストします。

# app/controllers/unread_responses_controller.rb
class UnreadResponsesController < ApplicationController
  def index
    @article = Article.find(params[:article_id])
    @responses = @article.unread_responses_for(@current_user)
  end
end
# app/views/unread_responses/index.json.jbuilder
json.array! @responses do |response|
  json.extract! response, :id
end
# config/routes.rb
Rails.application.routes.draw do
  resources :articles do
    resources :responses
    resources :unread_responses
  end
end

このエンドポイントは、未読レスポンスのIDのリストを返します。

# GET /articles/1/unread_responses.json
[{"id":1},{"id":2},{"id":3}]

ヒント: サーバー側で事前レンダリングできる動的コンポーネントを読み込む場合は、HTMLのレンダリングをサーバーサイドで行い、そのHTMLをJavaScriptで直接ページに注入する方が、通常は高速です。

未読レスポンスの表示

JavaScriptのコード内に未読レスポンス用エンドポイントのURLをハードコードする代わりに、ビューのdata属性にURLを設定しておき、後から参照できるようにします。

# app/views/articles/show.html.erb
<section id="responses" data-url="<%= article_unread_responses_path(@article, json: true) %>">
  # ...
</section>

ページのロード完了後、新しいエンドポイントに未読レスポンスIDをリクエストします。そして、そのリストのデータを使って、該当する要素にCSSクラスを追加することで各レスポンスを未読としてマークします。

// app/assets/javascripts/application.js
document.addEventListener("turbolinks:load", function () {
  responses = document.getElementById("responses");
 
  if (!responses.dataset.loaded) {
    Rails.ajax({
      url: responses.dataset.url,
      type: "GET",
      success: function (data) {
        responses.dataset.loaded = true;
 
        data.forEach(function (response) {
          element = document.querySelector(
            "[data-response-id='" + response.id + "']"
          );
          element.classList.add("unread");
        });
      },
    });
  }
});

このRailsアプリケーションはTurbolinksを使用しているため、turbolinks:loadイベントをリッスンしてページのロード完了を待ちます。イベントが発火したら、IDを使ってレスポンス一覧の要素を取得します。

次に、responses要素にloadedというdata属性が設定されているかどうかを確認します。この属性は未読レスポンスの更新後に設定するもので、ブラウザの戻るボタンなどでページが再表示された場合に余計なリクエストが発生しないようにするためです。

初回ロード時はloaded属性がまだ設定されていないため、エンドポイントへのリクエストが実行されます。成功したら、返却結果の各レスポンスについてIDで要素を特定し、「unread」CSSクラスを追加します。

まとめ:スプリンクルを振りかけよう!

再利用可能なコンテンツをフラグメントキャッシュし、JavaScriptをページに振りかけてユーザー固有の動的な要素を後から追加すれば、重要なコンテンツの大部分をキャッシュから直接配信できるため、アプリの初期レスポンスタイムを大幅に短縮できます。メインコンテンツのロード後に追加リクエストを行ってページを更新する手間はありますが、動的コンテンツを後回しにすることで、ユーザー固有の部分まで含めてすべてをキャッシュから配信する場合よりも総リクエスト時間が長くなっても、アプリの体感速度はむしろ向上します。

より高度な構成を目指すなら、スプリンクルパターンをフレームワーク化し、HTMLビューとJavaScriptを結びつけるライブラリ「Stimulus」をぜひチェックしてみてください。

本記事がRailsアプリケーションにおけるJavaScriptスプリンクルの入門としてお役に立てば幸いです。記事やブログ、その他取り上げてほしいトピックに関するご意見があれば、@AppSignalまでお気軽にお寄せください。

  1. Rubyでコマンドラインアプリケーション(CLI)を作る方法を徹底解説

    RubyはWebアプリケーション開発のイメージが強く、それ以外の用途があることを忘れられがちです。そこで本記事では、Rubyを使ってコマンドラインアプリケーション(CLI)を構築する方法を詳しく解説します。 まず、皆さんがよく目にするコマンドラインアプリケーションの例を挙げてみましょう。 psql(PostgreSQLの対話型ターミナル) rails bundler gem git CLIアプリケーションの作り方はさまざまですが、この記事では代表的な3つの方法に焦点を当てて紹介します。 この記事で学べること ARGV配列の使い方 OptParseライブラリの使い方 Thor gemの使い

  2. LiDAR技術の応用分野22選|農業から宇宙開発まで幅広い活用法を徹底解説

    前回のブログ「LiDAR技術とは何か?完全ガイド」では、LiDAR技術の基礎知識について解説しました。LiDARは、高密度かつ高精度の位置情報付き空間データを効率的に取得できることから、現在では最も効果的なマッピング手法のひとつとして認知されています。 本記事では、LiDAR技術が実際に活用されている分野を紹介します。各プロセスの適切な運営にどれほど大きな価値をもたらしているのか、その活用範囲の広さにきっと驚かされることでしょう。 LiDAR技術の主な応用分野 1. 農業 LiDARを使えば農地の地形図を作成でき、畑の傾斜や日照条件(日当たり)を把握できます。また、果樹園やブドウ園における作