Web開発の初歩 - ブラウザでのファイルキャッシュ

20th Jan, 2010 | nginx web

Webシステムではあちらこちらでキャッシュが使われている。クライアントサイドでのファイルのキャッシュ(ブラウザやproxy)や、サーバ側でのキャッシュ(memcacheとか)など、そこかしこでキャッシュが使われている。キャッシュする対象も、ファイルであったり、データベースの検索結果であったり、動的に生成したhtmlページであったり様々だ。

その中でも、今回はweb開発で基本となるユーザ側(ブラウザやproxyサーバ)での静的ファイル(Javascriptや画像など)のキャッシュについて書いてみる。ちなみに、以前nginxを使ったサーバサイドでの動的なhtmlコンテンツのキャッシュについても書いたのでそちらもどうぞ: nginxを使った簡単快速reverse proxy+cacheサーバ構築法

まず、説明するまでもなく、キャッシュとは、コストの高い処理の結果を保存しておき、再度同じリクエストがあった場合に同じ結果を返す仕組みのことだ。キャッシュというのものはWebシステムに限らずやっかいなもので、キャッシュが有効か無効かの判断(validation)が難しい。判断に失敗するとユーザは古いデータを使い続けてしまい、システム提供側の意図しないような動作になってしまう。システムのややこしい部分の大部分はキャッシュにあるんじゃないかというぐらいややこしいことが多い。

しかし、Webシステムにおけるブラウザ(or proxy)側でのキャッシュ戦略についてはいくつかの定番がある。

  1. ファイルが変更されたかどうかを最終ファイル更新日(Last-Modified)を元に毎回サーバに問い合せる (Apache等のWebサーバのデフォルトの機能はこれになっていることが多い)。そして変更があったときにのみ新しいファイルを返す。
  2. ファイルの有効期限(Expires)をある程度先(2週間後とか)に設定し、それまでの間は古いファイルが使われてしまっても仕方ないと諦める。
  3. ファイルの有効期限(Expires)を遠い未来(2037年とか)に設定する。そして、サーバ側でファイルの変更するときには、ファイル名も変更する。

1.は毎回サーバにアクセスしにいくので(ブラウザによっては必ず毎回ではないが)、そのための余分な時間がかかる。もちろん1個や2個ならたいしたことないが、1画面内に数十ファイルある場合それなりに違ってくる。また3と比較すると、壊れたブラウザやプロキシに対して弱い(後述する)。

2.はある程度古いコンテンツをユーザに流しても問題ないと言った緩い要求の場合には有効。ただ、1.と同じく、壊れたブラウザやプロキシには弱い。

3.は1, 2.と比べ、下記の理由で優れている。

  • 1.と違いファイルが変更したかどうかをサーバに問い合せる必要がないので、その通信の時間や帯域、サーバの負荷を低減できる。
  • 2.と違いファイルが更新された場合には、すぐにキャッシュが無効になる。
  • さらに、「ファイル名を変える」ということはブラウザやProxyから見ると「新規のファイル」になるのでvalidationの処理自体必要がなくなり、どんなことがあっても必ずサーバに問い合わすようになる。これは、キャッシュシステムが壊れていたり、バグがあったりしたときにもとても強い。ブラウザやproxyにキャッシュ関連のバグや、仕様という名のルール無視な製品によって、1や2がうまくいかずに、新しいファイルが配信されない、ということがそれなりにある。そういうブラウザやproxyを使っている人を無視できればいいのだけど難しいことも多い。また、ユーザ側で起こっていることをサーバ側から完全に知ることは基本的にはできないので、できるだけそういう問題がクリティカルにならないようにしておくことが大切。

ただし、3.はアプリケーション側でしなければいけないことが増えるという欠点がある(ファイル名が変わるということは、それを呼び出しているhtml等も変える必要がある等)。ちなみに、たいていの大手サイトでは静的ファイルは3.になっているし、YSlowPage Speedと言ったWebサイトの性能を計るようなソフトウェアでは、Expiresが設定されていないと評価が落ちる(警告が出る)ようにできている。YSlowやPage Speedと言ったツールも非常に面白いのでそのうち紹介したいと思っている。

実際の設定については、Webサーバソフトウェアやアプリケーションサーバによって変わってくるが、

  1. Last-modifiedヘッダーをつける(何もしなくてもこれがつくWebサーバやアプリケーションサーバがほとんど)。
  2. Expiresヘッダーのタイムスタンプをちょっと先にする。
  3. Expiresヘッダーを遠い未来(2037年)とかにする。また、ファイルを更新するときには名前を変える。ファイル名を変えるのが大変な場合には、query stringをつける、という手もある。例えば、http://www.example.com/picture.png?123456 みたいな感じにして123456のところをファイルの中身と連動して変える。ただし、query stringがついているキャッシュされなくなってしまうブラウザやproxyサーバがあるのは注意が必要。ただ「キャッシュされなくなる方」に誤動作する方が「古いファイルがキャッシュされ続けてしまうよりは」はマシなのでシステム的にこれが楽な場合はこれもありだろう。

おまけ:

この辺の設定もnginxだと非常に楽に書けて気に入っている。たとえば、これは実際のこのブログの設定だが、ブログ内に貼ってあるスクリーンショットなどの画像ファイルは中身によってファイル名を変えているので、3.のパターンが使え、Expiresを遠い未来にしている。

例:

location http://blog.madoro.org/mn/images/ {
  root /usr/local/projects/everblog/public/;
  if ($request_uri ~* "\/images\/[0-9a-f]+.(png|jpg$)") {
    expires max;
    break;
  }
}

nginxはこんな感じで、プログラム風に設定を書けるのがとても楽。一度使ってしまうとApacheのような設定が複雑なのには戻れない。

上記のように設定しFirebugのようなツールでヘッダーを確認するとExpiresが設定されているのを確認できる。

追記: twitterで聞かれたんで。これはキャッシュさせることができる(技術的なことよりも、仕様/要求的に)コンテンツが前提の話なので、キャッシュさせたくない、有効期限をちゃんと持たせたいファイルに対しては別の話になる。