30秒かかった画像表示を1.7秒に改善した話（imgix + S3 + CarrierWave）

**背景**
開発した自作アプリ（レシピ投稿サイト）のトップページで、画像表示があまりに重く遅かったため、初心者なりに改善できないか色々試行錯誤して、何とか改善することができました。今回はその記録と、日本語資料があまりないimgix（外部API）の使い方をまとめておきます！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/431a077d-6a94-db14-8f0b-ce58c2409459.gif" width="600">
👆ちなみにトップページはこんな感じです
　スライダーの画像も入れると20枚強くらいですね

## まず結果から
最初にトップページをAWSにデプロイした時、画像がなかなか表示されなかったので、GTmetrix（Webサイトのパフォーマンスを評価するツール）にかけてみました。そして、目が点になりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/0616cbfc-8f3b-fde8-c0bb-9b3cdd38e293.png" width="400">
**32.3秒**・・・！？
遅すぎる。さすがにこれは何とかしなければ。

色々と試行錯誤を重ね・・・結果、こうなりました👇
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/8c4367f0-cae9-2ada-6712-44efb577bffb.png" width="400">
おぉ、**1.7秒**・・・！
涙が出るほど嬉しかったです、、

念のため検証ツールのネットワークタブからも確認。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/91b7fa54-bf06-225e-b908-7010df1bc085.png" width="800")>
キャッシュ無効で671ms。問題なさそうです。

##大まかな流れ
1. CarrierWaveでの画像投稿を可能にする
1. CarrierWaveの投稿画像をS3に保存されるように設定
1. imgixを登録・設定

大まかな流れはこんな感じです。
技術的に未熟者でだいぶ遠回りしている感もありますが（特に#1）、その背景もこの後ご説明します、、、

なお、imgixだけ知りたい！
という方は、飛ばして#3からご覧ください（S3利用が前提です）

**環境**
ruby 2.6.3
rails 5.2.6
OS：Linux(CentOS)
IDE：Cloud9

## 実装の前に、経緯のお話
上記の流れになった背景を先に整理しておきたいと思います。
ここは私の試行錯誤の履歴ですので、手順だけご覧になりたい方は飛ばしていただいて構いません。

**〜imgixにたどり着くまで〜**
表示速度を上げるためにはじめに取り組んだのは、投稿画像の圧縮でしたが、ギリギリ許容できる画質まで落とした状態で約30秒かかっていました。しかも、お世辞にも良い画質とは言えなかったため、ユーザー目線で考えるとこれはちょっと、、という課題も新たに発生してしまいました。そもそも、ユーザーはスマホなどで撮影した画像をそのまま投稿するはずで、投稿サイトである以上、対応を考えなければいけません。

「違和感のない画質を維持しつつ、表示速度を改善する」

そんなこと初心者の私にできるのだろうか、、
と不安になりながら方法を探す中で、外部APIであるimgixにたどりつきました。
imgixの公式ページにはサービスについてこう書いてあります：

>imgix transforms, optimizes, and intelligently caches your entire image library for fast websites and apps using simple and robust URL parameters.

画像の最適化をしてくれ、かつキャッシュによってWebサイトを高速化できる。
画像最適化付きCDNという位置付けのようです。メリットとしては、以下のような点があげられます。

>1. 自サーバーの負荷が削減されることでWEBサーバーからのレスポンス速度低下を抑えることができる
>2. わざわざリサイズや圧縮せずとも、元画像を入れておき適切なパラメータを付与した画像URLにするだけで適切な画像を返してくれる

>[Japonlineの記事](https://www.japon-line.co.jp/tech/imgix%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E7%94%BB%E5%83%8Fcdn%E3%81%AE%E5%B0%8E%E5%85%A5/)より引用

元々見ていたのはCloudinaryというAPIだったのですが、導入難度がやや高めであるのに対し、imgixは画像URLを使うことで比較的容易に導入でき、公式が紹介しているrails向けのライブラリもあったので、imgixを使ってみることにしました。

**〜refileからCarrierWaveに乗り換え〜**
そこでまずimgix上で無料登録を済ませ、imxgix-railsの公式ドキュメント（[こちら](https://github.com/imgix/imgix-rails)）でRailsアプリへの導入方法を確認しました。

私は元々画像投稿にrefileを使用しており、refileとの併用も可能とは書いてあったのですが、refileはURLやパスをDBに保存しないので、imgix-railsとrefileを繋げるためのヘルパーメソッドの定義が必要と書かれていました。

そのヘルパーメソッドの記述内容も親切にGitHubに書かれているのですが・・・これが、お恥ずかしながら初心者の私には何をしているのかがよく理解できず。理解できていないものをコピペしてうまくいっても、本質的でないと思いました。CarrierWaveなら投稿画像にパスがつくため、接続のための事前設定なしに利用できるとも記載されていたため、思い切ってCarrierWaveに置き換えることにしました（これには賛否両論あるとは思いますが、初心者なりにできることを必死で考えた結果と、温かい目で見ていただければ幸いです、、）

以上の試行錯誤を経て、CarrierWaveで無事画像が表示されることを確認した上で、S3でバケットを作成し、画像がS3にアップロードされることも確認が取れたので、railsアプリケーション上でimgixの設定を行いました（そこに至るまで本当に苦労しました、、）

**〜CarrierWave+S3 で既に表示速度は大幅改善していたが、、〜**
外部ストレージを設定した時点で、既に速度自体は大幅に改善していたので、もはやimgixを導入しなくてもいいのではないかと思ったのですが、GTmetrixで確認したところGTmetrix Grade（総合評価）が「F」になっていました（元々はD〜Eだったので悪化）。愕然。

表示速度は改善していたのですが、PageSpeedInsightsにもかけてみたところ、「画像サイズが不適切」なことが主な問題なようでした。imgixの画像最適化によってこの部分は修正できそうだったため、導入後あらためて確認したところ、「C」ランクに改善し、圧縮後の粗かった画像も綺麗に表示されるようになりました。

**〜ちょっと余談〜**
なお、imgixは国内では一休.comや日経新聞で導入されているようです。
導入事例も含めimgixがどういうサービスかは[imgix という画像変換サービス(メモ)](https://note.com/chitapapa/n/n560938ae3845)によくまとまっています。

まだ国内では導入事例が少ないimgixですが、表示速度の改善方法を英語で調べていたことで、このAPIに早い段階で出会えたのはラッキーでした。

ところで、読み方はnginx風なのかと思いきや「 image・icks （イメジックス）」のようです。ずっと読み方わからなかったのですが、公式の解説動画でそう呼ばれていました。

#実装手順
CarrierWaveでの画像投稿（STEP1)及びS3バケットの作成（STEP2)については、[【Rails】 CarrierWaveチュートリアル](https://pikawaka.com/rails/carrierwave)を参考にしました。とても丁寧に解説されているので先にご一読をおすすめします。

（STEP1の内容はほぼ上記の記事をもとに書いています。割愛しても良いかと思ったのですが、一部記述内容が異なるのと、アップローダー名やカラム名をSTEP3以降の内容と揃えて記載しておいた方がわかりやすいと考えたため、上記記事の内容をかなり拝借しながら記述しております。ご了承ください）

ではそれぞれ見ていきましょう。

## STEP1. CarrierWaveでの画像投稿を可能にする
※すでにCarrierWaveを使用している人はSTEP2に飛んでください。

### 1-1. Gem追加

```Ruby:gemfile
gem 'carrierwave', '~> 2.0'
```

保存したらbundle installします。

### 1-2. Uploader追加

```:terminal
  rails g uploader アップローダー名

  例)rails g uploader PostRecipe
```

これで、```app/uploaders/post_recipe_uploader.rb```が作成されます。

ここには、アップロードするファイルの保存パスやサイズなどを指定することができます。
今回画像ファイルのサイズなどはimgix側で最適化してもらうので特に設定はしませんが、どのストレージにアップロードするかを指定しておきます。

```Ruby:app/uploaders/post_recipe_uploader.rb
class PostRecipeUploader < CarrierWave::Uploader::Base
  # Choose what kind of storage to use for this uploader:
  if Rails.env.production?
    storage :fog
  else
    storage :file
  end

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
end
```

３行目で、本番環境の場合は```storage :fog```（今回はS3)を、開発環境の場合は```storage :file```（アプリケーションの```public/uploads/モデル名/画像用カラム名/id```配下）を、画像の保存先に指定しています。開発環境で投稿した画像までS3で管理する必要はないので、明示的に分けています。

なお、```store_dir```では保存されるディレクトリを設定しています。
ここはデフォルトの表記のままで問題ありません。

開発環境とはいえ、public/uploads/配下に保存される画像がGitHubにあがるのはセキュリティの観点からもあまりよろしくないので、念のため```.gitignore```に追加しておきましょう。

```:.gitignore
/public/uploads
```

### 1-3. アップロード画像用のカラムを追加

```:terminal
rails g migration add_post_recipe_image_to_post_recipes post_recipe_image:string
```

アップロードする画像の情報を保存する```post_recipe_image```カラムを、PostRecipeモデルに作成します（カラム名やモデル名は適当な内容に修正してください）。なお、このカラムには画像データではなく、画像のファイル名が保存され、ビューで画像を表示する際には、「画像が格納されているパス」と「DBに保存されているファイル名」が使われます。

コマンドを実行したら、マイグレーションファイルが作成されるので、```rails db:migrate```しましょう。

### 1-4. コントローラーのストロングパラメーターに追記

post_recipes_controllerのストロングパラメーターに、先ほど作成した```post_recipe_image```カラムを追記します。

```Ruby:app/controllers/post_recipes_controller.rb
  def post_recipe_params
    params.require(:post_recipe).permit(
      :user_id,
      :title,
      :introduction,
      :post_recipe_image, #ここを追加
    #以下略
    )
  end
```

### 1-5. Uploaderクラスとカラムを紐づける

```Ruby:app/models/post_recipe.rb
  mount_uploader :post_recipe_image, PostRecipeUploader
```

先ほど作成した画像用カラムと、Uploaderの紐付けを行います。
紐付けを行うことで、画像アップロード時に、Uploaderに記述した諸設定を利用できます（例えば、アップロード時にどこに画像を保存するか等）。

画像用カラムを作成したモデル（今回の場合はPostRecipeモデル）ファイルに、上記を記述します。

### 1-6. ビューにファイル選択ボックスを追加

```HTML:app/views/post_recipes/new.html.erb
  <%= form_with model: @post_recipe, local:true do |f| %> 
     <div>
       <div class="from-group">
         <h6>写真をアップロード</h6>
         <%= f.file_field :post_recipe_image %>
       </div>
     </div>
   <% end %>
```

上記の```f.file_filed :カラム名```と記述することで、画像投稿が行えます。
もし編集画面もある場合は、同じように追記/修正しましょう。

なお、画像の表示については後ほどimgixのix_image_tagを使用するため、CarrierWaveの画像表示タグにする必要はありませんが、CarrierWaveで投稿した画像を表示するには、```<%= image_tag @post_recipe.post_recipe_image.url %>```のように書きます（post_recipe_imageの部分は、カラム名です）。

**refileから移行する場合**
モデル、コントローラー（ストロングパラメーター）、ビューなどのファイルにあるrefile関連の記述は削除し、refileの画像用カラムも削除しておきます。


## STEP2. CarrierWaveの投稿画像がS3に保存されるように設定

### 2-1. S3のバケット作成
S3のバケット作成については他に様々な記事で紹介されているため、割愛します。
私は「実装手順」の冒頭で紹介した[記事](https://pikawaka.com/rails/carrierwave)の「AWS設定」に沿って作成しました。

上記記事の「CarrierWave設定」以降は、記事と少々内容が変わってくるので、次の項でまとめます。

### 2-2. CarrierWaveにS3の設定を追加
まず、gemfileに```fog-aws```を追加します。
投稿画像の保存先を外部ストレージ（S3)にするのを助けてくれるgemです。
保存したらbundle installします。

```:gemfile
gem 'fog-aws'
```

👇次に、以下のファイルをコマンドで作成します。

```:terminal
touch config/initializers/carrierwave.rb
```

👇作成した```carrierwave.rb```に、S3バケット名とIAMユーザーの情報を記述します。

```Ruby:config/initializers/carrierwave.rb
require 'carrierwave/storage/abstract'
require 'carrierwave/storage/file'
require 'carrierwave/storage/fog'

CarrierWave.configure do |config|
  config.storage :fog
  config.fog_provider = 'fog/aws'
  config.fog_directory  = '作成したバケット名'
  config.fog_credentials = {
    provider: 'AWS',
    aws_access_key_id: Rails.application.credentials.aws[:access_key_id],
    aws_secret_access_key: Rails.application.credentials.aws[:secret_access_key],
    region: 'ap-northeast-1',
    path_style: true
  }
end
```

IAMユーザーの```aws_access_key_id```と```aws_secret_access_key```の値は、credentials.yml.encに記載します。

credentials.yml.encとmaster.keyは秘密情報を管理する仕組みで、環境変数を使わず秘密情報を管理できます。master.keyは、credentials.yml.encを複合化（暗号化されたデータを元に戻すこと）します。この仕組みについては、[【Rails】Rails5.2以降で追加された「credentials.yml.enc」について簡単にまとめてみた！](https://techtechmedia.com/whats-credential-yml/)が参考になります。

👇では、credentials.yml.encを編集し、IAMユーザーの情報を保存します。
直接エディタからは編集できないため、vimで記述します。

```:terminal
EDITOR=vim bin/rails credentials:edit
```

```Ruby:credentials.yml.enc
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: yyy

aws:
   access_key_id: xxx  #ここを追記
   secret_access_key: xxx  #ここを追記
```

xxxの部分に実際の値を記入し、escしてから:wqで保存します。
secret_key_baseはデフォルトの値のままです。

これでCarrierWaveでアップロードした画像が、S3に保存されます。


## STEP3. imgixを登録・設定

### 3-1. 会員登録
[公式サイト](https://dashboard.imgix.com/sign-up)より会員登録を行います。
3ヶ月間は$10のクレジットが付いてくるため、実質無料です（2021年6月時点）。

無料期間後の料金については、「[リアルタイム画像処理機能が充実した CDN、「imgix」 を試してみたらとても簡単で便利だった件](https://hyper-text.org/archives/2018/03/imgix_cdn.shtml)」が参考になります。

必須項目を入力してサインアップします。

### 3-2. Sourceの追加
サインアップ後、ログインするとDashboardが表示され、下図のような画面が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/5d3be3b4-3638-72ac-ff11-881d535194c2.png" width="450">

ADD A SOURCEをクリックし、Sourceを作成します。
ちなみに、Sourceは複数作成できます。

- **General**
    - **Source Type:** 「AmazonS3」を選択
- **AWS Settings**
    - **Access Key ID:**  IAMのアクセスキーの値を入力
    - **Secret Access Key:**  IAMのシークレット・アクセスキーの値を入力 
    - **S3 Bucket:**  S3バケット名を入力
    - **Path Prefix:**  「uploads」を入力
- **Domains**
    - **imgix Subdomain:**  imgixのサブドメインとなる値を自由に入力

他の項目は未入力で問題ありません。
入力できたらSAVEをクリックし、デプロイされたらSourceの設定は完了です。

### 3-3. Railsアプリケーションとimgixを接続
開発環境に戻ります。
👇まず```application.rb```の```module アプリ名```内に、以下を記述します。

```Ruby:config/application.rb
    Rails.application.configure do
      config.imgix = {
        source: ENV['IMGIX_SOURCE']
      }
    end
```

sourceには、先ほどimgix上で設定したsourceのサブドメインを入力します。
念のため、環境変数化しておきます。

```Ruby:.env
IMGIX_SOURCE="yyy.imgix.net(sourceのサブドメイン)"
```

👇次に、```storage.yml```に以下を追記します。
Active Storageに認識させるS3の情報が、imgixのsourceと同じになるように設定します。

```:config/storage.yml
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: ap-northeast-1（AWSリージョン名）
  bucket: S3バケット名
```

これでimgixがS3に保存されているマスター画像にアクセスできるようになりました。

### 3-4. ビューファイルにix_image_tagを追記
imgix-railsの公式ドキュメントにあるように、```ix_image_tag```は、imgixが画像のリサイズ、クロップなどを行う上で必要なパラメーターを渡してくれるヘルパーメソッドです。

```:app/views/homes/top.html.erb
<%= ix_image_tag(@post_recipe.post_recipe_image.path, url_params: { w: 150, h: 150, fit: 'crop' }, tag_options: {class: 'rounded-circle'}) %>

<%= link_to ix_image_tag(@post_recipe.post_recipe_image.path, url_params: { w: 150, h: 150, fit: 'crop' }, tag_options: {class: 'rounded-circle'}), post_recipe_path(@post_recipe.id) %>
```

👆```post_recipe_image```の部分はCarrierWaveの設定で追加した画像用カラム名を指定します。
画像にリンクを貼る場合は、```link_to```を用いればOKです。
classやalt属性を指定する場合は```tag_options```内に、リサイズの値やクロップの指定は```url_params```内に記述します。

ちなみにfallbackの画像を表示したい場合は、```assets/images```配下にfallback用の画像を格納し、以下のように記述すれば、画像が投稿された場合とそうでない場合とで表示を区別できます。

```:app/views/homes/top.html.erb
<% if @post_recipe.post_recipe_image.blank? %>
 <%= image_tag('no_recipe_image.jpg', size: '70', class:'rounded-circle') %>
<% else %>
 <%= ix_image_tag(@post_recipe.post_recipe_image.path, url_params: { w: 70, h: 70, fit: 'crop' }, tag_options: {class: 'rounded-circle'}) %>
<% end %>
```

これで本番環境にデプロイし、画像がうまく表示されていれば成功です。
最後に、デプロイ時に.envファイルを手動で転送するのを忘れないようにしましょう（これを転送してあげないと、imgixのsourceが読めずにエラーになります）。


**終わりに**
refileで記述していた時は、画像の見せたい部分がうまく表示されないこともありましたが、imgixで最適化したことでうまくクロップされ、画質も格段に良くなりました。

さらに、S3と併用したことで、表示速度もアップすることができました。私が探した限りでは、「Railsアプリ x imgix x S3」の導入方法は日本語の記事が出回っていなかったため、今回整理してみました。何かのお役に立てれば幸いです。お疲れ様でした！


## 参考資料
https://github.com/imgix/imgix-rails

https://note.com/chitapapa/n/n560938ae3845

https://hyper-text.org/archives/2018/03/imgix_cdn.shtml

https://qiita.com/yamamoto_shuji/items/5afd9ffe13f36ff29677

https://techtechmedia.com/whats-credential-yml/

https://pikawaka.com/rails/carrierwave