猫でもわかるWebプログラミングと副業

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

LGTMoonの画像のレスポンスヘッダにCache-Controlを追加した

LGTMoonとは

LGTM画像を簡単に作れるサービスです。

LGTMoon - 最もシンプルなLGTM画像ジェネレーター

LGTMoonの画像保存/配信の仕組み

LGTMoonは画像をバイナリにしてPostgreSQLのImageテーブルに保存しています。

画像に対するリクエストを受けた場合は (例: http://lgtmoon.herokuapp.com/images/2045Scalaでこのリクエストを受け、DBからバイナリを読み出して返しています。 *1

class ImageBinaryController extends Controller {
  /** idを受け取り画像のバイナリデータを返す */
  def image(id: Long)  = Action.async { request =>
    // DBから引っ張ってくる
    ImageRepository.image(id).map {
      case Some(image) => {
        image.bin match {
          case Some(bin) => {
            // image/png としてレスポンスを返す
            Result(
              header = ResponseHeader(200),
              body = Enumerator.fromStream(new ByteArrayInputStream(bin))
            ).withHeaders(CONTENT_TYPE -> "image/png")
          }
          case None => NotFound("Not Found")
        }
      }
      case None => NotFound("Not Found")
    }
  }
}

フロントでの「最近の画像」表示の仕組み

フロントではvue.jsを使い、最近の画像一覧取得API(Recent API)を叩いて返ってきた結果をDOMに落とし込んでいます。 Recent APIは最新20枚の画像のURLを返すので、10秒に1回このRecent APIを叩き、結果をそのままvue.jsにかませてDOMを表示しています。

つまり、10秒に1回、imageタグを一旦全部消して、Recent APIを叩き、imageタグを追加、というステップを行っています。

レスポンスヘッダのCache-Controlについて

さて、いままで画像のレスポンスのヘッダにはCache-Controlは付けていませんでした。Cache-Controlとは キャッシュについて整理してみた - Qiita あたりを参考にしていただければわかります。ブラウザ側に「この画像はキャッシュしないでくれ」「この画像は1時間までならキャッシュしていいよ」などと伝えることができます。

Cache-Control を指定していなかった場合の問題

Cache-Controlを指定していなかった場合、キャッシュの動作について、ブラウザごとに細かい動作の違いがありました。

Google Chrome

Google Chrome において、Recent APIを叩いた結果同じ画像が存在した場合、画像へのリクエストは発生しません。

f:id:yoshiki_utakata:20161222163548p:plain

Safari

今回問題になったのはSafariでした。Safariにおいて、Recent APIを叩いた結果同じ画像が存在した場合でも、画像へのリクエストが発生します。 つまり、10秒に1回、画像20枚分のリクエストが発生することになります。

本当の原因はSafariの実装を知らなければ分かりませんが、おそらく、Cache-Controlを指定しない場合、Safariは画像をいい感じにキャッシュしてくれないのかなーという感じです。

Cache-Control を Scala Play で指定する

そこで、Cache-Controlで、画像を1時間キャッシュしろという命令をレスポンスヘッダに含めました。 *2 Scala Play で Cache-Control をレスポンスヘッダに含める方法は以下の通りです。

/** 画像のバイナリデータを返すコントローラー */
class ImageBinaryController extends Controller {
  /** idを受け取り画像のバイナリデータを返す */
  def image(id: Long)  = Action.async { request =>
    ImageRepository.image(id).map {
      case Some(image) => {
        image.bin match {
          case Some(bin) => {
            Result(
              header = ResponseHeader(200),
              body = Enumerator.fromStream(new ByteArrayInputStream(bin))
            ).withHeaders(
              CONTENT_TYPE -> "image/png",
              CACHE_CONTROL -> "max-age=3600") // 1時間キャッシュしろ
          }
          case None => NotFound("Not Found")
        }
      }
      case None => NotFound("Not Found")
    }
  }
}

これを行うことにより、Safariでも画像のキャッシュが効くようになり、Recent APIを叩いた後での画像へのリクエスト数が激減しました。 さらに、Chromeなどでもページをリロードした際には積極的にキャッシュが使われるようになります。

f:id:yoshiki_utakata:20161222164417p:plain

今回の画像は一度作られたら変わらないものなので、キャッシュ時間をさらに長くすることも可能です。

まとめ

HTTPレスポンスヘッダーのCache-Controlについて学んだお話でした。