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」を構築しました。詳しくはこちらをご覧ください。

関連記事

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ