Railsアプリを『浅く』パフォーマンス・チューニングしてみる(その2)


前回は『N+1件』問題を解決して、パフォーマンスを改善することができました。今回はページ表示部分(View)に関する処理を変更することでパフォーマンスを改善したいと思います。

まずは、前回終了時点のパフォーマンスを確認しておきましょう。

パフォーマンス2開始時点

だいたい1秒弱かかっているようですね。そういえば前回、クライアントマシンのスペックを記載するのを忘れていました・・・筆者は Mac Book Air 2012 mid (デュアルコア2.0GHz Intel Core i7)/メモリ8G を使用しています。

それでは、チューニングを初めましょう!

パフォーマンス・チューニング2:Viewヘルパー

さらなるパフォーマンスの改善を行うためには、どの処理に負荷がかかっているのかを知ることが不可欠です。rack-mini-profilerの表示を確認し、当たりをつけましょう。『959.0ms』という部分をクリックします。

プロファイラ詳細チューニング前

どうやら『books/index』のレンダリングに一番時間がかかっているようですね。そのbooks/indexの処理の中で『_book』パーシャルをレンダリングしているようです。ちょっとbooks/indexのソースを確認しておきましょう。

📄books/index.html.erb
<table class="bookshelf">
  <tr>
    <th class="bookshelf" colspan="999">Bookshelf</th>
  </tr>
  <% @books.each_slice(7) do |line_of_books| %>
  <tr>
    <% 7.times do |col| %>
      <td>
        <%= render 'book', book: line_of_books[col] if line_of_books[col] %>
      </td>
    <% end %>
  </tr>
  <% end %>
</table>

@books配列の中から7冊ずつ切り出して、各列ごとにパーシャルを呼び出しています。このロジック自体は特に問題なさそうです。では次に、パーシャル『_books』のソースを見てみることにします。

📄books/_book.html.erb
<div class="book" data-description="<%= book.description %>">
  <div class="title">
    <%= link_to book.title, book %>
  </div>
  <div class="author"><%= book.author.name %></div>
</div>

特に変わったことはしていませんね。。。何それ、これ以上チューニングできる箇所は無いということ?

・・・いいえ、実はできるんです。

どこをチューニングするかというと、ズバリ3行目です。

📄Viewヘルパー
    <%= link_to book.title, book %>

Bookコントローラのshowアクションに対するリンクを生成するヘルパーです。1つ目の引数はリンク表示する文字列で、2つ目の引数は遷移先のURLです。URLに『book』を指定していますが、これは book_path(book) と記述するのと同じです。ルーティング定義通りにパスを構築してくれる便利なヘルパーなんですが、実はこれが非常に重い処理なんです。

というわけで、URLを直接指定してみましょう。

📄URLを直接指定
    <%= link_to book.title, "/books/#{book.id}" %>

Rails野郎にとっては、なんだかすごくダサいですね。。。色々と思う所はあるものの、実際にどれぐらい速くなったか確認してみることにします。

チューニング2−1後

*おおっと*

約『300ms』も速くなってる!

単純計算すると、ダミーデータは1,000件なので、1回のパス構築ヘルパー呼び出し毎に0.3ms速くなったということです。「たったそれだけ?」と思うかもしれませんが、普通はこういったリンクをいくつも使用すると思うので、それらをかき集めると結構なパフォーマンス改善になるはずです。

ただし、このチューニング方法はURLを直接指定しちゃってます。なので将来URLが変わった時に、このソースも変更しなければなりません。つまり保守性が下がってしまうというトレードオフのチューニング方法なのです。全てのページでURLを直接指定するのではなく、保守性の低下とパフォーマンス改善を天秤にかけた上で判断したほうが良いでしょう。

パフォーマンス・チューニング3:erbメソッド化

トレードオフのチューニングに手を染めてしまったついでに、もうひとつトレードオフなチューニングを施します。さきほどの『books/index』のソースを再度確認します。

📄books/index.html.erb
<table class="bookshelf">
  <tr>
    <th class="bookshelf" colspan="999">Bookshelf</th>
  </tr>
  <% @books.each_slice(7) do |line_of_books| %>
  <tr>
    <% 7.times do |col| %>
      <td>
        <%= render 'book', book: line_of_books[col] if line_of_books[col] %>
      </td>
    <% end %>
  </tr>
  <% end %>
</table>

パス構築ヘルパーと同様に処理が重いのが『render』です。 使わない日は無いというぐらい使用頻度の高いヘルパーメソッドですが、何度も呼び出すとかなりの負荷になるようです。確認のためにrenderでパーシャルを呼び出すのをやめて、パーシャル内のコードを移行して来てみましょう。

📄books/index.html.erb
<table class="bookshelf">
  <tr>
    <th class="bookshelf" colspan="999">Bookshelf</th>
  </tr>
  <% @books.each_slice(7) do |line_of_books| %>
  <tr>
    <% 7.times do |col| %>
      <td>
        <% if book = line_of_books[col] %>
          <div class="book" data-description="<%= book.description %>">
            <div class="title">
              <%= link_to book.title, "/books/#{book.id}" %>
            </div>
            <div class="author"><%= book.author.name %></div>
          </div>
        <% end %>
      </td>
    <% end %>
  </tr>
  <% end %>
</table>

もともと『_book』パーシャルにあったコードを持って来ただけです。renderが排除できたところで、パフォーマンスを確認してみましょう。

render排除後パフォーマンス

ジーザス!

なんと、『150ms』程度になりました!500msもパフォーマンスが改善しましたよ。しかしこれって・・・パーシャルを使うなということ?

いくら何でも、パーシャルを使わずに1つのファイルに長々とコードを書くのはいただけません。同等のパフォーマンスを保ちつつ、パーシャルを使用できる方法は無いのでしょうか?

そんな欲張りな貴男のリクエストにお答えするのが『def_erb_method』です。これはerbファイルをレンダリングするインスタンスメソッドを定義するためのメソッドです。これを使えばrender呼び出しすることなく、パーシャル(のような感覚で別ファイル)を使用することができます。

それではまず、ヘルパーを作成しましょう。『BooksHelper』を編集します。

📄books_helper.rb
module BooksHelper
  extend ERB::DefMethod

  def_erb_method('render_book(book)', "#{Rails.root}/app/views/books/_book.html.erb")
end

def_erb_methodの1つ目の引数がメソッド名とその引数、2つ目の引数がパーシャルへのパスになります。

次に、『books/index』を編集します。

📄books/index.html.erb
<table class="bookshelf">
  <tr>
    <th class="bookshelf" colspan="999">Bookshelf</th>
  </tr>
  <% @books.each_slice(7) do |line_of_books| %>
  <tr>
    <% 7.times do |col| %>
      <td>
        <% if book = line_of_books[col] %>
          <%= raw render_book(book) %>
        <% end %>
      </td>
    <% end %>
  </tr>
  <% end %>
</table>

先程定義したメソッド『render_book』を呼び出すように変更します。結果はHTMLコードで返ってくるので、rawメソッド 呼び出しが必要です。erbメソッドを登録したので、アプリケーションサーバの再起動が必要です。再起動したら、さっそくパフォーマンスを確認してみましょう。

チューニング2−2完了

むほっ!

パフォーマンスを維持できていますね!render排除のために1つのファイルに長々とコードを書かなくて済みます。いいですね。

ただし1つ注意点があります。それはパーシャル内のコードを変更した場合、アプリケーションサーバを再起動しなければならないということです。ですので開発当初は通常のrenderで書いておき、パフォーマンス・チューニングの工程になってから、erbメソッド化することをオススメします。

サーバサイドは随分と高速になりました。次回はクライアント側のパフォーマンス・チューニングを実施したいと思います。

[追記 2015/9/20]

本記事はRAILS_ENV=developmentの環境で検証した結果に基いています。最近rails 4.2 + production環境で同様の検証をしてみたのですが、production環境だとdef_erb_methodを使わなくとも、かなりのパフォーマンスが出ていました。ですので、def_erb_methodを使っても劇的にパフォーマンスが向上することは無さそうです。(とはいえ、速くなっていることには変わりないのですが。1,000件ループrenderで400msが200msになるぐらい)

今回使用したソースコード

githubに使用したソースコードを置いておきます。試してみたい方はどうぞ。

https://github.com/itmammoth/ome-tuning

タグ「V1_pt」が本記事最初の状態、「V2_pt」がチューニング後の状態です。

関連する記事


「Railsアプリを『浅く』パフォーマンス・チューニングしてみる(その2)」への10件のフィードバック

  1. itmammoth

    ありがとうございます!
    すごく励みになります。

  2. renderで極端に遅いのはproduction環境だけだという噂を聞いたのですが、実際のところどうなんでしょうね…

  3. ↑の質問をさせていただいた者です。
    production環境ではなくdevelopment環境ですね!

  4. itmammoth

    「renderメソッド」がですか?
    config.assets.debug = true の件で遅くなるのは知っていますが、renderについては聞いたことがありませんでした。
    もしよければ、情報ソースを教えていただけないでしょうか?

  5. お早いお返事ありがとうございます。
    情報ソースは会社の上司のエンジニアさんです。真偽の程がわからないので噂と書かせて頂きました。もし本当なら私も詳しく知りたいです!

  6. itmammoth

    そうですか。確かに興味深いですね。
    ちょっと今時間がないので・・・時間ができたときにでも検証してみるかもです!

  7. ピンバック: rack-mini-profiler でパフォーマンスの解析をする方法 [Rails] – Site-Builder.wiki

  8. ピンバック: def_erb_method を使って render を高速化する方法 [Rails] – Site-Builder.wiki

  9. ピンバック: パフォーマンス [Rails] – Site-Builder.wiki

コメントは受け付けていません。