CarrierWaveでのファイル表示が遅いときに確認すること

Railsでのファイル管理にCarrierWaveを利用しています。

添付されたファイルのうち
- ファイル名
- ファイルのURL
- ファイルの種類

をフロントへのレスポンスによく使用していました。

で、20ファイル程度の一覧表示にやたら時間がかかっており、解決策を探っていました。
試行錯誤した結果、5秒弱かかっていたレスポンスが00ミリ秒程度になったので、それまでの流れを書いておきます。

## どこが遅いのかを調査

パフォーマンス測定にNew Relicを利用しています。
New Relicを見てみると、開発環境でも以下のようなグラフになっていました。
![スクリーンショット_2023-07-21_15_42_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/bd6ca295-3f56-4a81-1e8e-b50e37f0dc5e.png)

濃い赤い部分が、ファイル情報の取得にかかっていることを示しています。
DBとかよりも圧倒的に、ファイル情報の取得に時間がかかっていました。

## 対策1: ストレージから直接取得ではなくCDNを介す
このアプリケーションでは、AWSを利用しています。
ストレージはS3だったので、CloudFrontを介すようにしました。

S3単体と、S3+CloudFrontの構成はそれほど料金変わらないことが多いです。
メリットばかりなのでこれは基本やっておくと良いと思います。

CarrerWaveからCloudFrontを利用する場合、`config/initializers/carrierwave.rb`ファイルに
```ruby
config.asset_host = Rails.application.credentials.aws[:cloudfront_host]
```
というコードを追加します。

`config.asset_host`にCloudFrontのホスト名を入れています。

## 対策2: キャッシュの有効化

キャッシュを設定すると、ファイル更新しても即時に反映されない懸念は多少発生します。
が、carrierwaveを利用していると、割とその心配が影響する機会は少ないと思います。

carrierwaveのデフォルトの設定では、ファイルが保存されるディレクトリは、以下のようにレコードのIDごとに作成されます。
```ruby
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
```
例えばファイルのパスは
`"uploads/posts/thumbnail/1/**.png"`や
`"uploads/posts/thumbnail/2/**.jpg"` となります。


つまり「ファイル更新しても即時に反映されない」が起きる条件は、
「同じレコードに、同じファイル名」がアップされたときのみになります。

さらに、CarrierWaveのver.3では、同じファイル名がアップされると、`image(2).png`のように変換されます。
よって、同じファイル名がアップされてもキャッシュによって最新のファイルが反映されない問題も起こらなくなる見込みです。

なのでCloudFront同様キャッシュも基本的に設定しておくと良いと思います。

`config/initializers/carrierwave.rb`に以下の設定を追加します。

```ruby
config.fog_attributes = { cache_control: "public, max-age=#{10.minutes.to_i}" }
````
するとアップロードされたファイルをAWSコンソールのS3などでみると`Cache-Control`が設定されるようになります。
![スクリーンショット 2023-07-21 15.57.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/7d7be51d-38ab-377a-3621-c6c7be6d228f.png)



## 対策3： 可能ならファイル情報の取得を回避

CarrierWaveのファイルがあるカラムを引くと、以下のような情報が取得されます。

```ruby
Post.last.thumbnail
=> #<PostThubmnailUploader:0x000000010ce565e8
 @cache_id=nil,
 @file=
  #<CarrierWave::Storage::Fog::File:0x000000010cc16af8
   @base=#<CarrierWave::Storage::Fog:0x000000010ce55bc0 @uploader=#<PostThubmnailUploader:0x000000010ce565e8 ...>>,
   @content_type=nil,
   @path="uploads/post/thumbnail/********/dog.png",
   @uploader=#<PostThubmnailUploader:0x000000010ce565e8 ...>>,
 @filename=nil,
 @identifier="dog.png",
 @model=
  #<Post:0x000000010d2fdf38
   id: "********",
   name: "わんちゃん",
   thumbnail: "dog.png",
   created_at: Thu, 20 Jul 2023 17:01:11.645417000 JST +09:00,
   updated_at: Thu, 20 Jul 2023 17:01:11.645417000 JST +09:00>,
 @mounted_as=:thumbnail,
 @staged=false,
 @storage=#<CarrierWave::Storage::Fog:0x000000010ce55bc0 @uploader=#<PostThubmnailUploader:0x000000010ce565e8 ...>>,
 @versions={},
 @versions_to_cache=nil,
 @versions_to_store=nil>
```

ここに載っている情報は割とすぐに取得できます。
ただし、始めに記載した

> 添付されたファイルのうち
> - ファイル名
> - ファイルのURL
> - ファイルの種類
> 
> をフロントへのレスポンスによく使用していました。

のうち、ファイルの種類は先ほどの中には載っていません。
メソッドは用意されており、`Post.last.thumbnail.content_type`で取得可能です。

が、それを行うと、ファイル情報取得のための通信が発生します。
私の場合は、fog-awsと言うgemを介して行われます。

通信が発生した結果先ほどのコマンドを実行すると、`@file` の情報が追加で取得できています。

```ruby
Post.last.thumbnail
> #<PostThubmnailUploader:0x000000010ce565e8
 @cache_id=nil,
 @file=
  #<CarrierWave::Storage::Fog::File:0x000000010cc16af8
   @base=
    #<CarrierWave::Storage::Fog:0x000000010ce55bc0
     @connection=
     @uploader=#<PostThubmnailUploader:0x000000010ce565e8 ...>>,
   @content_type=nil,
   @directory=  <Fog::AWS::Storage::Directory
    key="insight-sky-development-files",
    creation_date=nil
  >,
   @file=
      <Fog::AWS::Storage::File
    key="uploads/post/thumbnail/********/dog.png",
    cache_control="public, max-age=600",
    content_disposition=nil,
    content_encoding=nil,
    content_length=17123,
    content_md5=nil,
    content_type="image/png",
    etag="********",
    expires=nil,
    last_modified=2023-07-20 08:01:13 +0000,
    metadata={"x-amz-id-2"=>"********", "x-amz-request-id"=>"********"},
    owner=nil,
    storage_class=nil,
    encryption="********",
    encryption_key=nil,
    version=nil,
    kms_key_id=nil,
    tags=nil,
    website_redirect_location=nil
  >,
   @path="uploads/post/thumbnail/********/dog.png",
   @uploader=#<PostThubmnailUploader:0x000000010ce565e8 ...>>,
 @filename=nil,
 @identifier="dog.png",
 @model=
  #<Post:0x000000010d2fdf38
   id: "********",
   name: "わんちゃん",
   thumbnail: "dog.png",
   display_order: 3,
   created_at: Thu, 20 Jul 2023 17:01:11.645417000 JST +09:00,
   updated_at: Thu, 20 Jul 2023 17:01:11.645417000 JST +09:00>,
 @mounted_as=:attach_file,
 @staged=false,
 @storage=
  #<CarrierWave::Storage::Fog:0x000000010ce55bc0
   @connection=
:headers=>{"User-Agent"=>"fog-core/2.3.0"}, :persistent=>false,... @_excon_sockets={#<Threa
d:0x00000001002cb158 run>=>{}} @persistent_socket_reusable=true>>>,
   @uploader=#<PostThubmnailUploader:0x000000010ce565e8 ...>>,
 @versions={},
 @versions_to_cache=nil,
 @versions_to_store=nil>
```

`@file`からcontent_typeも取得できます。
が、ファイル名やURLの取得に比べて、この`@file`を取得するためには通信の時間要します。
ファイルの表示が遅い時は、これを回避した対策を考えるのも有効な手段です。

## 対策4 その他

NginxでのGzip。
これもよく言われる対策の1つです。設定もそれほど難しくないのでやっておくと良いかもしれません。
CarrierWaveを中心とした問題とは少し外れる気もしたので、その他に分類しています。


画像ファイルの圧縮も有効です。
が、このアプリケーションでは画像以外にもPDFやWord, Excelなどさまざまなファイルを扱っています。
ファイルに応じて処理が変わることを今の段階では避け、圧縮の手段をまだとることはしていません。


## 終わりに
私が関わっているアプリケーションでは上記の3つの対策を行い、20ファイル程度の一覧表示に、5秒弱かかっていたレスポンスが200ミリ秒程度になりました。

同じように困っている方の参考になればなと。