それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

HTTPキャッシュ。なぜ嫌われるのか。

忌み嫌われるキャッシュたち。

キャッシュはどうやら、世間では嫌われ者のようです。

ScrenCaptured_2016-03-05_0.54.33

どうして、そんなにキャッシュされるのがイヤなんだろうか。

そもそもキャッシュってなんだっけ?

キャッシュとは、更新されていないコンテンツ(画像、CSS、JS、HTML、DNS結果など)を何度も何度も取得行かずに済むように、クライアントPC側で保存し再利用する仕組み。

つまり、転送量の節約。無駄な転送を控える。非常にエコな仕組みであります。

HTTPのエコ。HTTPはエコなプロトコルだったはず。

3つのR です。 Reduce Reuse Remix 。複数のファイルをそれぞれ、別途管理して1つのページとして構成(Remix)する仕組みです。

ブラウザのキャッシュを利用するメリット

通信料の節約、画面表示の高速化、戻るボタン対応など。

ブラウザは、サーバーからのレスポンスヘッダを見てコンテンツをキャッシュするか判断する。またキャッシュから再利用するか判断する。

検索結果ですらキャッシュで良いよね。ていうかキャッシュ可能あってほしい。

頻繁に更新される検索結果だとキャッシュしないと「戻るボタンで先頭Offsetが狂ってしまうことがある。」(2ページ目問題)

HTTP キャッシュを正しく使って便利にして欲しい。

キャッシュを正しく使ってほしい。願わくば「とりあえずキャッシュオフ」的な記述は少なくなってほしい。

キャッシュを使うほうが閲覧者にも回線にも優しいわけです。

はじめに HTTPのGET/POST

HTTPのGETとPOSTは別の意味を持つ。

  • GET サーバーにあるファイルの取得
  • POST サーバーにデータを送信し更新する。

なのでPOST時には戻るボタンを始め、キャッシュが効かないのが正しい動作であるといって過言でないはず。

HTTP GET でのキャッシュ

HTTP GETではファイルを取得表示する。ファイルはimg script style video などあらゆるデータを指す。

一旦取得したファイルは再利用したい。そのためのキャッシュ。HTTP GETはファイル取得だからキャッシュするべき。

ブラウザにキャッシュさせる

たとえば、画像ファイルをプログラムから配信する場合を例に考えてみる。

php で画像を配信する際にキャッシュを有効にする。

<?php
$file_name = "some_image_file_name";
//画像タイプ判別
$type_id   = exif_imagetype($file_name);
$type_name = image_type_to_mime_type($type_id);
$last_modified = filectime($file_name);//最終更新日を作成(キャッシュ用)
//キャッシュを利用させる
header("Content-type: " . $type_name);
header("Last-Modified: " . gmdate('r', $last_modified));
header("Cache-Control: max-age=".(60*60*24*10)); // 10日キャッシュしていい
//画像を返す。
echo file_get_contents($file_name);

ここでは、HTTP 1.1 と使うので、 Cache-Controle に max-age を指定しキャッシュ可能であることを明治。且つ、ブラウザにファイルの最終更新日時を指定している。

且つ、ファイルの最終更新日を伝えている。

すると、ブラウザは、MaxAgeの期間キャッシュ利用し、利用期間が切れた時に、キャッシュ更新を確認に来る。

サーバー側の動作

次回リクエストではブラウザが更新確認のために、If-Modified-Sinceを送信してくる

サーバー側のプログラムで、このIf-Modified-Sinceの値とファイルの更新日時を比較し、ファイルのほうが新しければ改めてファイルを送信する。

Apacheなど正しく実装されたHTTPサーバーでは、静的ファイルで処理を自動的におこなってくれる。

PHPなどの動的ファイルでは、プログラマが面倒を見る必要がある。

PHPでブラウザのキャッシュ確認依頼に答える。

<?php
$file_name = "some_image_file_name";
//画像タイプ判別
$type_id   = exif_imagetype($file_name);
$type_name = image_type_to_mime_type($type_id);
$last_modified = filectime($file_name);//最終更新日を作成(キャッシュ用)
///もしキャッシュ更新確認リクエストなら
if(isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])){
    $str_time = $_SERVER["HTTP_IF_MODIFIED_SINCE"];
    $last_modified_since = strtotime($str_time);
    if($last_modified_since == $last_modified){
        //ファイル更新がなければ、キャッシュ有効を返す。
        header("HTTP/1.1 304 image not modified");
        header("Cache-Control: max-age=".(60*60*24*10)); // 追加10日キャッシュしていい
        exit;
    }
}

キャッシュ確認を受け取った時の処理は、サーバー側で304(Not Modified)を送信しつつ、キャッシュの利用期限を明示する。

その他のキャッシュ制御ヘッダ

キャッシュ関連のヘッダでよく使うものをまとめておこうと思う。

pragma/expires

  • Pragma これはHTTP1.0用
  • Expires これはHTTP1.0用

ExpiresでもMaxAgetでも同じだけど、Expiresは期限日時を計算してで送信するんので若干めんどくさい、MaxAgeで良いよね。

もし、ExpiresとMaxAgetの両方がセットされたらMaxAgeが優先

いまどき、HTTP1.0のみブラウザは存在しないので MaxAgeを書いておけば十分。(もちろんレスポンスにHTTP/1.1 書くべき。)

public / private

  • public プロキシや通信経路でキャッシュしていいよ。
  • private ブラウザ以外キャッシュしたダメ

public はHTTPが非暗号化通信である利点を活かしていいよって意味、デフォルト(省略時)はPrivate。

etag / none-match

  • if-none-match
  • etag

Etag はファイルの最終更新日の代わりに、適当なファイルID(ハッシュ値)を入れておく。

ブラウザとサーバーはファイルの更新日時を使う代わりにetag を使う。

ハッシュ値の計算は、衝突してもたかだキャッシュリクエストなのでmd5sum程度で十分だね。

たとえば、ファイルを複数台のミラーサーバーに送信すると、サーバー毎に同一ファイルで更新日時が違うファイルが出来る。

E-tagはサーバーミラーやDB内部データなど更新日時で判断できない時のため。

ETag / Last-Modified は単なるキー

最終更新日・ファイルIDは単なる「変数」、この値を元にブラウザとサーバーは、キャッシュの更新確認を行う。

両方がセットし送信されると、優先度とかは特にない。

両方についてブラウザがチェックする。つまりif-none-matchとif-modifed-sinceが送信される。サーバーは両方受け取ったら、サーバー側で「両方が一致」することを確認して、レスポンスするのが望ましい。(個人的にはEtagつけたら更新日時は省略でも良いと思うけど。)

MaxAget・Expiresがキャッシュの「有効期限」

ブラウザはキャッシュの有効期限まで、ブラウザ内に溜め込んだキャッシュを利用する。

期限が切れたらサーバーにキャッシュの有効性を確認する。「期限切れだけど、サーバーで更新合った?」と確認する。

サーバーは更新がアレば「コレが新しいファイルだよ受け取ってね 200 ok 」無ければ、「更新なかったよ 304 not modified」と答える。

ブラウザにキャッシュを毎回確認だけはさせる

MaxAget=0かつ、Last-Modifiedを送信する。(もしくはMaxAge=0 なら入れる意味が無いのでCache-controlを省略する。

するとブラウザはキャッシュの利用期間が切れた状態で受け取るので、毎回更新確認を送る。

サーバー側でファイル更新されてない(304)を送信すると、キャッシュが利用される。

キャッシュで困ったら、LastModifiedを入れて、If-Modified-Sinceに304を正しく応答しておけば良いんじゃないかなと。

キャッシュされて鬱陶しんだけど!!と言われたら

「Ctrl+F5押せ!」と言って下さい。

もしくは開発ツール開いてる時はキャッシュオフのチェックを入れておく

開発ツールを開いてる時はキャッシュオフ。

キャッシュされて鬱陶しいだけのためにHTML(静的ファイル)にcache オフを埋め込むのは筋違いではないかと思います。

キャッシュされないサイトはレスポンスタイムも転送量(転送料)も嵩むし、貴重なユーザ時間を無駄にさせてる。

静的ファイルのキャッシュを強制OFF

キャッシュは「URL」単位に行われるので、GETパラメーターをつけて別のURLとする。

<?php
echo "<script src=http://takuya-1st.hatenablog.jp/scripts.js?"+time()+" ></script>"

などとタイムスタンプをつけておけば、出力されるファイルのリンク先は、毎回異なる物になる。

http://takuya-1st.hatenablog.jp/scripts.js?1457111252
http://takuya-1st.hatenablog.jp/scripts.js?1457111253
http://takuya-1st.hatenablog.jp/scripts.js?1457111254

毎回タイムスタンプをつけておけば、別のファイルとしてブラウザにキャッシュされる。そのためキャッシュが再利用されない。

戻るページ対策

「戻るボタン」押したら、ページが無効ですログインしなおして下さい。

などと無駄な努力をしているページがある。

ページ遷移をすべてPOSTで行うようなJSを仕込めば済む話だ。

2ページ目問題。

頻繁に更新されるコンテンツのページングは悩ましいですよね。

データベースに33件あるとします。これを「投稿日時の降順」で表示していた場合。

ページ データベース
1 ページ目 1-10
2 ページ目 11-20
3 ページ目 21-30
4 ページ目 31-33

ユーザーが1ページ目から2ページ目に遷移した時。11-20 が表示されるわけですが。

ユーザーが1ページ目の閲覧中に、書き込みが10件やってきたとする。

すると

ユーザー閲覧中のページ番号 最初のデータベース 更新後ページ番号 更新後DB コメント
1' ページ目 1-10 追加された書き込み10件
1 ページ目 1-10 2' ページ目 11-20 同じページが表示されてしまう。
2 ページ目 11-20 3' ページ目 21-30
3 ページ目 21-30 4' ページ目 31-40
4 ページ目 31-33 5' ページ目 41-33

このようなことが起きてしまい、ユーザーが混乱する。

なので、キャッシュ期間をある程度設けないと、ユーザーが戻る進むするたびに検索結果が変化してしまう。

この場合は、サーバー側ユーザ・セッションに一旦検索結果をある程度キャッシュしておくのがベーターであろうと思う。

逆にいえば、コレほどの頻度でないと、キャッシュオフにする意味が無いのではないか。

参考資料

https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers

https://www.mnot.net/blog/2007/05/15/expires_max-age

http://stackoverflow.com/questions/14496694/whats-default-value-of-cache-control