Rails7をちょっと試す(turbo_frame_tag でページの表示を速くする編）

## はじめに

Rails 7.0.0 には、 [Turbo](https://github.com/hotwired/turbo-rails) が組み込まれています。
今回はその中で、 `turbo_frame_tag` を試してみたいと思います。
`turbo_frame_tag` を使ってページの表示速度を改善してみます。

以下のバージョンで確認しました。

- Ruby 3.0.2
- Rails 7.0.3.1

## 前準備をする

今回は前準備に少し手間がかかります。

## Rails プロジェクトを作る

新しくRailsプロジェクトを作ります。このとき `
(データベースオプションを指定しているのに深い意味はありません。)

```shell
mkdir rails_sandbox
cd rails_sandbox
rails new . --database=postgresql
```

## model を2つ作る

`User` と `Book` の2つの model を作ります。

```shell
bin/rails g model User name
bin/rails g model Book title
```

## seed データを作る

`User` と `Book` それぞれ1つずつデータを作ります。

```ruby:db/seeds.rb
User.create(name: 'Taro')
Book.create(title: 'Ruby on Rails')
```

## データベースを作成して、seed データを登録する

データベースを作成して、seedデータを登録します。

```shell
bin/rails db:create db:migrate db:seed
```

## Dashboard コントローラと index View を作成する

User と Book の両方のデータを表示するためのコントローラとViewを作ります。

```shell
bin/rails g controller Dashboard index
```

## Dashboard#index メソッドを実装する

`Dashboard#index` を実装します。
`Dashboard#index` の実行が開始された時刻を画面に表示するため、 `@index_at` に現在時刻を設定します。
`User.long_time_all` と `Book.long_time_all` はわざと時間がかかるようにこの後、model側で実装します。

```ruby:app/controllers/dashboard_controller.rb
  def index
    @index_at = Time.zone.now
    @users = User.long_time_all
    @books = Book.long_time_all
  end
```

## long_time_all をscopeで定義する

`scope` を使って2つのmodelの `long_time_all` を定義します。
sleep を使ってデータの検索の時間がかかるように見せかけます。
User と Book で sleep の時間を変えておきます。
Userは検索に１秒かかり、Bookは２秒かかるように見せかけます。

```ruby:app/models/user.rb
  scope :long_time_all, lambda {
    sleep 1
    all
  }
```

```ruby:app/models/book.rb
  scope :long_time_all, lambda {
    sleep 2
    all
  }
```

## View を作成する

User と Book の両方のデータを表示する View を作成します。

### Dashboard のページ

`Dashboard#index` の実行開始時刻を表示します。
User と Book の表示は、 partial を使います。

最後に現在時刻を表示します。

```erb:app/views/dashboard/index.html.erb
<h1>Dashboard#index</h1>
<p>index controller start at <%= @index_at %></p>

<h2>Users</h2>
<%= render partial: 'users' %>
<h2>Books</h2>
<%= render partial: 'books' %>

<p>rendered index at <%= Time.zone.now %></p>
```

### User と Book データの View

単純に `user.name` を表示します。
最後に現在時刻を表示するようにします。

Book についても同様に実装します(Bookについては省略)

```erb:app/views/dashboard/_users.html.erb
<% @users.each do |user| %>
  <p>
  <%= user.name %>
  </p>
<% end %>
rendered users at <%= Time.zone.now %>
```

## 表示してみる

Rails サーバーを `bin/rails s` で起動して、 http://localhost:3000/dashboard/index にアクセスします。
だいたい3秒後にページが表示されます。
Userの検索に1秒、Bookの検索に2秒かかるので合計で大体約3秒かかって表示される計算です。

![before01_000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209941/2f3fe248-f8e8-891e-f930-eef2f9bdbe24.png)
上のスクリーンショットから以下がわかります。
- controller の開始が 05:22:08
- Usersのrender が 05:22:11
- Booksのrender が 05:22:11
- ページ全体のrenderが 05:22:11

つまり、controllerの開始(05:22:08)からページ全体のrender(05:22:11)まで3秒経過しています。

次のスクリーンショットを見れば、最初に index の読み込みに3.09秒かかっていることがわかります。
![before02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209941/1cd4410e-a767-15ce-71b3-7c28e8fd0d4b.png)



## ここからが本番

ここからが本番です。
Rails7 から使えるようになった `turbo_frame_tag` を使ってページの表示を速くしてみましょう。
User と Book のデータの表示する部分を `turbo_frame_tag` を使って書き換えて速度を改善してみます。

## ルーティングを追加する

User の表示と Book の表示を非同期にするため、 User と Book のデータを取得するためのルーティングを追加します。

```ruby:config/routes.rb
  get 'dashboard/users'
  get 'dashboard/books'
```

## Dashboard#users と Dashboard#books コントローラを追加する

`Dashboard#index` から User と Book のデータ取得する処理を削除し、別に `users` と `books` メソッドを追加します。

```ruby:app/controllers/dashboard_controller.rb
  def index
    @index_at = Time.zone.now
  end

  def users
    @users = User.long_time_all
  end

  def books
    @books = Book.long_time_all
  end
```

## View を変更する

### dashboard/index のView の変更

dashboard/index ページの User のデータの表示箇所を変更します。
`turbo_frame_tag` を使って `/dashboard/users` に GET でアクセスし、そのレスポンスを表示するように変更します。
レスポンスが返されるまでは、 `loading...` と表示するようにします。

Book のデータの表示も同様に変更します。
```erb:app/views/dashboard/index.html.erb
<h2>Users</h2>
<%= turbo_frame_tag :users, src:'/dashboard/users' do %>
  loading...
<% end %>
<h2>Books</h2>
<%= turbo_frame_tag :books, src:'/dashboard/books' do %>
  loading...
<% end %>
```

### Dashboard#users Dashboard#books が返す response のテンプレートを作成する

`Dashboard#users` が返すレスポンスで使用するテンプレートを作成します。

`turbo_frame_tag` を使います。
`turbo_frame_tag` の引数は、 `app/views/dashboard/index.html.erb`  の `turbo_frame_tag` の引数で指定したのと同じ `:users`にします。同じにすることで、 `app/views/dashboard/index.html.erb` の `turbo_frame_tag` で囲った部分がレスポンスで差し替えられます。
`turbo_frame_tag` のblockの中身は既存の `app/views/dashboard/_users.html.erb` を利用します。


```erb:app/views/dashboard/users.html.erb
<%= turbo_frame_tag :users do %>
  <%= render partial: 'dashboard/users', collection: @users %>
<% end %>
```

`Dashboard#books` が返すレスポンスで使用するViewのテンプレートも同様に作成します。

## 再度アクセスする

http://localhost:3000/dashboard/index にアクセスしてみましょう。

今度はページがすぐに表示されて、Users 部分が1秒後に、Book が2秒後に表示されるのがわかります。
3秒かかっていたページの表示が2秒に短縮されました。

![turbo_frame.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209941/650eb0f6-9fbc-c4e6-0cec-9efa95541602.gif)
上のアニメーションgifから以下がわかります。
- controller の開始が 04:50:49
- Usersのrender が 04:50:50
- Booksのrender が 04:50:51
- ページ全体のrenderが 04:50:49

つまり、controllerの開始(05:22:08)からページ全体が表示されるまで(04:50:51)まで2秒経過しています。

次のスクリーンショットを見れば、最初のindexの読み込みは、20msで終わり、users が1.01秒、booksが2.01秒かかっていることがわかります。users と books の読み込みは非同期に実行されるため、結果的におよそ1秒短縮できました。
![after01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209941/28294c59-2b3f-c3e6-5c5c-39ecd7de5e75.png)

## まとめ

Turbo をうまく活用することでページの表示を速くできました。
従来であれば、JavaScriptでfetchして、 User と Book のデータをAPI経由で json 形式で受け取って、ページの一部を JavaScript で書き換えるという方法になると思います。
ですが、ここでは、JavaScript を１行も書かずに（裏ではJavaScriptが動いていますが、）同じことを実現できているところがポイントです。JavaScriptを使わない Rails によるWebアプリが今後増えていくのかどうか気になるところです。

## 補足事項

今回は、 ~~手抜きで~~ 話を簡単にするために ルーティングを dashboard/users と dashboard/books にしましたが、本来は、 dashboard/users/index や dashboard/books/index にするとか、users/index と books/index にするとか、それぞれのリソースに合わせて REST にするのが Rails way なやり方かも知れません。

Hotwire の機能については色々と今後も触っていきたいと考えています。

## 参考情報

- [turbo-rails/app/helpers/turbo/frames_helper.rb](https://github.com/hotwired/turbo-rails/blob/v1.0.0/app/helpers/turbo/frames_helper.rb)

今回試したコードは以下で確認できます。

- [turbo_frame_tag 使用前](https://github.com/suketa/rails_sandbox/tree/try_rails7_turbo_frame_loading_lazy_before)
- [turbo_frame_tag 使用後](https://github.com/suketa/rails_sandbox/tree/try_rails7_turbo_frame_loading_lazy_after)