エンジニアブログといいつつも技術ネタが少ないので、私からは弊社サービスのコンピテンシークラウドで課題としてあった、画像を返す処理が重いという問題をどのようにして解決したか紹介したいと思います。
304 Not Modified
前提として、次のようなことをRails でやっていました。
- ユーザー画像をアップロードしてAWS S3に保存
- 同じグループに属する人だけそのユーザー画像を閲覧することができる
しかし、
- アクセスがあるたびにS3 から画像を取得して返すので重い
という問題がありました。 コードとしては次のようなものです。
class UsersController < ApplicationController authorize_resource # 権限チェック通った人だけ画像を返す def avatar uploader = @user.avatar send_data( uploader.read, type: uploader.content_type, disposition: 'inline', ) end end
これをstale?を使うことによって、
- 304 Not Modified を返してくれる
- ついでに Last-modifiedとEtag も
@user.updated_at
を使って返してくれる
ようになります。あと、 Cache-Control も設定するようにしたものが次のコードです。
class UsersController < ApplicationController authorize_resource def avatar # 実際にはファイル更新時間などを使ってetag, last_modified を設定しないと、ファイル以外の更新でも `@user.updated_at` が変わり etag, last_modified の値も変わってしまう return unless stale?(@user, template: false) # 明示しておかないとsend_data が `Cache-Control: private` にしてしまう # ファイルの更新頻度がわかっているのであれば有効期間を伸ばしてあげればよい expires_in 0, public: false, must_revalidate: true uploader = @user.avatar send_data( uploader.read, type: uploader.content_type, disposition: 'inline', ) end end
これだけで、ブラウザがキャッシュしてくれるようになるはずです。 確認は request spec を使ってこのようにかけます。
require 'rails_helper' RSpec.describe UsersController, type: :request do let(:user) { create(:user, :with_avatar) } before { login_as(user) } describe 'GET /users/:id/avatar' do it "ETag によってレスポンスが変わること" do get avatar_user_path(user) expect(response).to have_http_status(:ok) expect(response.body).to be_present expect(response.content_type).to eq 'image/jpeg' expect(response.cache_control).to eq max_age: "0", private: true, must_revalidate: true etag = response.etag get avatar_user_path(user), params: {}, headers: { "If-None-Match": etag } expect(response).to have_http_status(:not_modified) expect(response.body).to be_blank expect(response.content_type).to be_nil expect(response.cache_control).to eq max_age: "0", private: true, must_revalidate: true # ETag を変えるためにupdated_at を更新する Timecop.freeze(1.second.since) do user.touch end get avatar_user_path(user), params: {}, headers: { "If-None-Match": etag } expect(response).to have_http_status(:ok) expect(response.body).to be_present expect(response.content_type).to eq 'image/jpeg' expect(response.cache_control).to eq max_age: "0", private: true, must_revalidate: true expect(response.etag).not_to eq etag end it "Last-Modified によってレスポンスが変わること" do get avatar_user_path(user) expect(response).to have_http_status(:ok) expect(response.body).to be_present expect(response.content_type).to eq 'image/jpeg' expect(response.cache_control).to eq max_age: "0", private: true, must_revalidate: true last_modified = response.last_modified get avatar_user_path(user), params: {}, headers: { "If-Modified-Since": last_modified.rfc2822 } expect(response).to have_http_status(:not_modified) expect(response.body).to be_blank expect(response.content_type).to be_nil expect(response.cache_control).to eq max_age: "0", private: true, must_revalidate: true # Last-Modified を変えるためにupdated_at を更新する Timecop.freeze(1.second.since) do user.touch end get avatar_user_path(user), params: {}, headers: { "If-Modified-Since": last_modified.rfc2822 } expect(response).to have_http_status(:ok) expect(response.body).to be_present expect(response.content_type).to eq 'image/jpeg' expect(response.cache_control).to eq max_age: "0", private: true, must_revalidate: true expect(response.last_modified).not_to eq last_modified end end end
積極採用
株式会社あしたのチームでは、「人事評価制度」を一緒につくっていく仲間を募集しています。 ご興味のある方は、こちら!