LGTMoonとは
LGTM画像を簡単に作れるサービスです。
LGTMoon - 最もシンプルなLGTM画像ジェネレーター
LGTMoonの画像保存/配信の仕組み
LGTMoonは画像をバイナリにしてPostgreSQLのImageテーブルに保存しています。
画像に対するリクエストを受けた場合は (例: http://lgtmoon.herokuapp.com/images/2045) Scalaでこのリクエストを受け、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を叩いた結果同じ画像が存在した場合、画像へのリクエストは発生しません。
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などでもページをリロードした際には積極的にキャッシュが使われるようになります。
今回の画像は一度作られたら変わらないものなので、キャッシュ時間をさらに長くすることも可能です。
まとめ
HTTPレスポンスヘッダーのCache-Controlについて学んだお話でした。