問題
User一覧画面ページネーションはしていたが、表示までに20秒ほどかかっていた。
メモリ食い過ぎでサーバーが落ちることもあった
結論
一覧画面に不必要なモーダルの切り分け
ハイパーN+1問題
を解決することで表示時間が1秒ほどになった。
詳細
一部抜粋したコード(説明に不必要な箇所は省いています)
CustomersController
def index
@customers = Kaminari.paginate_array(Customer.all).page(params[:page]).per(40)
end
<% @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を修正。
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万件近くになったため、以下のように修正を行った。
CustomersController
def index
# 戻した
@customers = Kaminari.paginate_array(Customer.all).page(params[:page]).per(40)
end
# showに切り分け
def show
@customer = Customer.find(params[:id])
end
<%= render partial: 'customer', collection: @customers %>
<% @customers.each do |customer| %>
<div class="modal fade" id="customer_show_modal"></div>
<tr>
<td>
<%= customer.mail %>
</td>
<td>
<%= customer.foobar %>
</td>
<td>
<%= customer.hoge %>
</td>
<td>
<%= link_to '詳細', customer_path(customer) %>
</td>
...etc
</tr>
$("#customer_show_modal").html("<%= escape_javascript(render 'customer_show',
customer: @customer
) %>")
$('#customer_show_modal').modal('show')
<%= 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 %>
非同期でモーダルの中身を表示することにした。
$("#customer_show_modal").html("<%= escape_javascript(render 'customer_show',
customer: @customer
) %>")
$('#customer_show_modal').modal('show')
結果的に無駄なincludes等もなくなりcontrollerもスッキリした
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はシンプルに実装したほうが良い。
今回のケースだと、そもそも詳細をモーダルで表示する必要があるのか?等、考えたほうがいいとも思った。