LoginSignup
1

More than 1 year has passed since last update.

Railsのヘヴィな重い一覧ページを改修したお話。

Last updated at Posted at 2022-06-07

問題

User一覧画面ページネーションはしていたが、表示までに20秒ほどかかっていた。
メモリ食い過ぎでサーバーが落ちることもあった

結論

一覧画面に不必要なモーダルの切り分け
ハイパーN+1問題
を解決することで表示時間が1秒ほどになった。

詳細

一部抜粋したコード(説明に不必要な箇所は省いています)

customers_controller.rb
CustomersController
  def index
    @customers = Kaminari.paginate_array(Customer.all).page(params[:page]).per(40)
  end
index.html.erb
<% @customers.each do |customer| %>
  <tr>
    <td>
      <%= customer.mail %>
    </td>
    <td>
      <%= customer.foobar %>
    </td>
    <td>
      <%= customer.hoge %>
    </td>
    <td>
      <button type="button" class="btn btn-primary" data-toggle="modal" data-tarfet="#customer_<%= customer.id %>_modal">
        詳細
      </button>
    </td>
    ...etc
  </tr>
  <div class="modal fade" id="customer_<%= customer.id %>_modal">
    <%= customer.foobar.name %>
    <%= customer.bar.hige.name %>
    <%= customer.bar.huga.name %>
    ...etc
  </div>
<% end %>

前々からこの一覧ページが重いと言われていた。

まずは、N+1検出のbulletの指摘対応で以下のようにcontrollerを修正。

customers_controller.rb
  def index
-   @customers = Customer.all
+   @customers = Kaminari.paginate_array(Customer.includes(:foobar, bar: [:hige, :huga]).page(params[:page]).per(40)
  end
  • foobarテーブル
  • barテーブル
  • higeテーブル
  • hugaテーブル
    をincludesしてN+1の対応を行った。一旦これで表示までの時間が早くなった。

しかしcustomersのレコードが増えるにつれ上記テーブルのレコードも増えていき
単純にレコード取得にも時間がかかるようになった。
customersの数が1万件近くになったため、以下のように修正を行った。

customers_controller.rb
CustomersController
  def index
    # 戻した
    @customers = Kaminari.paginate_array(Customer.all).page(params[:page]).per(40)
  end
  
  # showに切り分け
  def show
    @customer = Customer.find(params[:id])
  end
index.html.erb
<%= render partial: 'customer', collection: @customers %>
<% @customers.each do |customer| %>
<div class="modal fade" id="customer_show_modal"></div>
_customer.html.erb
  <tr>
    <td>
      <%= customer.mail %>
    </td>
    <td>
      <%= customer.foobar %>
    </td>
    <td>
      <%= customer.hoge %>
    </td>
    <td>
     <%= link_to '詳細', customer_path(customer) %>
    </td>
    ...etc
  </tr>
show.js.erb
$("#customer_show_modal").html("<%= escape_javascript(render 'customer_show',
  customer: @customer
) %>")
$('#customer_show_modal').modal('show')
_customer_showl.html.erb
  <%= customer.foobar.name %>
  <%= customer.bar.hige.name %>
  <%= customer.bar.huga.name %>
  ...etc

解説

renderの修正

ページレンダリングのパフォーマンス改善として
最初に目についたのが以下の部分

<% @customers.each do |customer| %>
  <tr>
    <td>
      <%= customer.mail %>
    </td>
    <td>
      <%= customer.foobar %>
    </td>
    <td>
    ~~
<% end %>

collectionに対してrenderを行うのではなく
renderに対してcollectionを渡すほうがパフォーマンスは良くなる。詳細はここらへん参考に
今回は丁寧に書いたがrails likeに書くなら以下のようにも書ける。

  # 以下の二つは同等
  <%= render partial: 'customer', collection: @customers %> 
  <%= render @customers %>

モーダルの改善

次に改善した部分はモーダルの部分。
一覧画面からページ遷移を挟まず、モーダルで詳細情報を確認するために
あらかじめcollection数分のmodalの内容をhtmlに含めていた。

<% @customers.each do |customer| %>
  <tr>
   <%= customer~~~ %>
  </tr>

  # この部分が人数分htmlに含まれていた。(10000人分
  <div class="modal fade" id="customer_<%= customer.id %>_modal">
    <%= customer.foobar.name %>
    <%= customer.bar.hige.name %>
    <%= customer.bar.huga.name %>
    ...etc
  </div>
<% end %>

非同期でモーダルの中身を表示することにした。

show.js.erb
$("#customer_show_modal").html("<%= escape_javascript(render 'customer_show',
  customer: @customer
) %>")
$('#customer_show_modal').modal('show')

結果的に無駄なincludes等もなくなりcontrollerもスッキリした

customers_controller.rb
CustomersController
  def index
    # 戻した
    @customers = Kaminari.paginate_array(Customer.all).page(params[:page]).per(40)
  end
  
  # showに切り分け
  def show
    @customer = Customer.find(params[:id])
  end

結果

一覧画面で何でもかんでもやろうとするのが、railsっぽくない実装に繋がったり
余計な処理追加でパフォーマンス悪くなりがちなので
出来るだけviewはシンプルに実装したほうが良い。

今回のケースだと、そもそも詳細をモーダルで表示する必要があるのか?等、考えたほうがいいとも思った。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1