Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: render_async gemでレンダリングを高速化(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: render_async gemでレンダリングを高速化(翻訳)

Railsのコントローラにコードを追加するときに、さまざまな問題が発生することがあります。コントローラのアクションは非常に長くなることがあり、処理も複雑になりがちです。他の問題として、データ量の増加にともなってページの読み込みが重くなることもよくあります。コントローラのアクションにコードを追加した結果、一部のアクションで実行に失敗するとレンダリングが行われず、ユーザーに悪印象を残してしまうこともあります。

私たちのSemaphoreでもこの種の問題に何度か遭遇しました。通常は、コントローラのアクションをより小さなアクションに分割し、素のJavaScriptで非同期レンダリングすることで解決しています。

しばらくやっているうちに、これをrender_asyncというRailsページ高速化gemに切り出せることに気づきました。このgemは、RailsサーバーへのAjax呼び出しを行ってコンテンツを非同期にHTMLに読み込みます。

問題1: 時間とともに重くなる

新しいコードを足すと、Railsコントローラのアクションは肥大化します。気をつけておかないと、コードの量やデータ量の増加にともなってページの読み込みがだんだん重くなります。

問題2: アクションをブロックするコードへの対処

コントローラに新しいコードを追加する際、ビュー全体のレンダリングのためにコントローラのアクション内で追加のデータも読み込まないといけなくなることがあります。

ここで、アクションのレンダリングがブロックされてしまうコードの実例を見てみましょう。

movies_controller.rbのshowアクションで、データベースから映画を1件フェッチし、それに対応するレーティングも外部のIMDBから読み込みたいとします。

class MoviesController < ApplicationController
  def show
    @movie = Movies.find_by_id(params[:id])

    @movie_rating = IMDB.movie_rating(@movie)
  end
end

find_by_idで映画を読み込む部分は普通のコードです。ここでは自分たちが管理するデータベースから映画を1件検索しようとしています。

しかし映画のレーティングの読み込み部分では、外部IMDBへの追加リクエストに対して回答を受け取れることを当てにしているため、この外部サービスが不能になったりダウンしたりしたときに問題が生じます。こうなるとMoviesController#showがダウンし、本来の映画の読み込みまでできなくなってしまいます。

解決方法

render_async gemを使うことで、この2つの問題を解決できます。このgemは、Railsページのレンダリング完了後にコンテンツを非同期で読み込みます。

ページにHTMLを追加する既存のJavaScriptコードの代わりにrender_asyncを使うのはなぜでしょうか。その理由は、JavaScriptでの退屈なフェッチやリプレースをrender_asyncが代行してくれるからです。

render_asyncの動作

特定の映画と外部サービスから取得したレーティングの詳細を表示するapp/views/movies/show.html.erbというファイルがあるとします。

render_asyncを使う前のコードは次のとおりです。

# app/views/movies/show.html.erb

<h1>Information about <%= @movie.title %></h1>

<p><%= @movie.description %></p>

<p>Movie rating on IMDB: <%= @movie_rating %></p>

render_asyncを使えば、次のように書けます。

# app/views/movies/show.html.erb

<h1>Information about <%= @movie.title %></h1>

<p><%= @movie.description %></p>

<%= render_async movie_rating_path(@movie.id) %>

show.html.erbの読み込み後に、render_asyncによって映画のレーティングのセクションが読み込まれます。このページではmovie_rating_pathへのAjaxリクエストがjQueryで作成され、AjaxのレスポンスがページのHTMLでレンダリングされます。

render_asyncは特定のパスへのリクエストを作成するので、config/routes.rbにそのパスを追加しておく必要があります。

# config/routes.rb

get :movie_rating, :controller => :movies

ルーティングで指定したアクションをコントローラにも追加する必要があります。ここではmovies_controller.rbにmovie_ratingアクションを追加します。

# app/controllers/movies_controller.rb

def movie_rating
  @movie_rating = IMDB.movie_ratings(@movie)

  render :partial => "movie_ratings"
end

このmovie_ratingではパーシャルをレンダリングするので、対応するパーシャルも作成する必要があります。

# app/views/movies/_movie_rating.html.erb

<p>Movie rating on IMDB: <%= @movie_rating %></p>

もっとも肝心なのは、アプリケーションレイアウトにcontent_forタグを追加しておくことです。その理由は、render_asyncがAjaxレスポンスを処理するコードをそこに埋め込むためです。

# app/views/layouts/application.html.erb

<%= content_for :render_async %>

外部のIMDBサービスがダウンまたは応答停止した場合は、映画のレーティング情報以外のコンテンツがページに読み込まれるので、ページの他の部分は外部サービスの影響を受けなくなります。

まとめ

今回の例では、Railsページのレンダリングをrender_asyncで高速化するために以下を行いました。

  1. MoviesControllershowアクションをシンプルにしてテストや読み込みをやりやすくした。
  2. 映画レーティングのマークアップをパーシャルに移動した(Railsではおすすめのパターン)。
  3. 外部サービスが落ちてもshowアクションがブロックされないようにした。

render_asyncの追加機能や改良案がありましたら、render_asyncまでプルリクかissueをお送りください。

ご意見やご感想がありましたら、元記事のコメント欄にどうぞ。この記事がお役に立ちましたらぜひ共有をお願いします。


お知らせ: Semaphoreではスピードこそすべてであり、CIシステムを高速かつ快適にするのが私たちのミッションです。顧客と多くのやり取りや経験を重ね、あらゆるテストスイートを自動並列化して数分で完了できる新しいCI「Semaphore Boosters」を構築しました。詳しくはこちらをご覧ください。

関連記事

Railsの`CurrentAttributes`は有害である(翻訳)

Rails向け高機能カウンタキャッシュ gem「counter_culture」README(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。