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

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

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

## 詳細

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

```ruby:customers_controller.rb
CustomersController
  def index
    @customers = Kaminari.paginate_array(Customer.all).page(params[:page]).per(40)
  end
```

```erb: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を修正。

```diff_ruby: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万件近くになったため、以下のように修正を行った。


```ruby: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
```

```erb:index.html.erb
<%= render partial: 'customer', collection: @customers %>
<% @customers.each do |customer| %>
<div class="modal fade" id="customer_show_modal"></div>
```

```erb:_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>
```

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

```erb:_customer_showl.html.erb
  <%= customer.foobar.name %>
  <%= customer.bar.hige.name %>
  <%= customer.bar.huga.name %>
  ...etc
```

## 解説
### renderの修正
ページレンダリングのパフォーマンス改善として
最初に目についたのが以下の部分
```erb
<% @customers.each do |customer| %>
  <tr>
    <td>
      <%= customer.mail %>
    </td>
    <td>
      <%= customer.foobar %>
    </td>
    <td>
    ~~
<% end %>
```

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

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

### モーダルの改善
次に改善した部分はモーダルの部分。
一覧画面からページ遷移を挟まず、モーダルで詳細情報を確認するために
あらかじめcollection数分のmodalの内容をhtmlに含めていた。
```erb
<% @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 %>
```

非同期でモーダルの中身を表示することにした。
```js:show.js.erb
$("#customer_show_modal").html("<%= escape_javascript(render 'customer_show',
  customer: @customer
) %>")
$('#customer_show_modal').modal('show')
```

結果的に無駄なincludes等もなくなりcontrollerもスッキリした
```ruby: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はシンプルに実装したほうが良い。

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