キャッシュされた古いバージョンの静的ファイルが使用されないように最新であることを示すバージョンを付加する

Page updated :

The page you are currently viewing does not support the selected display language.

前段が長いので、プログラムだけ見たい人は読み飛ばしてください。

環境

Visual Studio
  • Visual Studio 2019
ASP.NET Core
  • 3.1 (Razor ページ, MVC)

Web ページのファイルのキャッシュについて

Web ページにアクセスした際に HTML の他に .js ファイル (Javascript) や .css ファイル (スタイルシート)、画像ファイルがクライアントにダウンロードされ、表示や実行が行われます。 しかし画像ファイルの中にはサイズが大きいものもあり、毎回ダウンロードするとコストが大きくなる場合があります。 また、.js ファイルや .css ファイルについては同じファイルが他のページでも使いまわされていることが多く、毎回ダウンロードする意味がなかったりするため、 これらのファイルはダウンロード後ブラウザに一時的に保存されることがあります。 これが キャッシュ と呼ばれるものです。

ファイルがキャッシュされている場合、例えば同じページを訪問した際に HTML は Web サーバーからダウンロードされますが、 他のファイルについてはキャッシュがある場合はダウンロードせずにローカルに保存されているファイルを使用して表示、実行されます。 これによりサーバーの負荷が下がり、クライアント側は通信帯域を消費せずに高速にページを表示することが可能です。

キャッシュによって発生する問題

サーバーに配置されているファイルとキャッシュされているファイルが同じかどうかは、一般的には HTML に記述されているファイルパスに依存します。 ファイルパスが同じならは基本的にはキャッシュされたファイルが使用されることが多いです。(条件は他にもある場合がありますがファイルパスは確実です)

以下はファイルパスの例です。href や src の属性パラメータが該当します。

<!-- スタイルシート (.css) -->
<link rel="stylesheet" href="/css/site.css" />

<!-- 画像ファイル -->
<img src="/image/sample.png" alt="サンプル画像" />

<!-- JavaScript (.js) -->
<script src="/js/site.js"></script>

しかし、この動作が行われた場合に問題になることがあります。

例えば、利用者が初回アクセス時にバージョン1の JavaScript ファイルあった場合、バージョン1の JavaScript ファイルがキャッシュされます。 その後、サーバー側でバージョン2の JavaScript ファイルが公開され、 利用者が同じページにアクセスしたときに JavaScript ファイルのパスが同じだった場合、サーバーのバージョン2の JavaScript ファイルがダウンロードされずにキャッシュされているバージョン1の Javascript ファイルが使用されてしまう場合があります。 これにより公開側が意図していない不具合が発生してしまうことがあります。

この時、利用者側の対応策のひとつで「キャッシュを削除してみてください」というのをよく目にすると思いますが、これが原因になっている可能性があるからです。

サーバー側でキャッシュによる問題への対応策

前述の通り、キャッシュされるファイルが使用されるかどうかはファイルパス(URL)に大きく依存します。

そのため、ファイルを更新したときはファイル名を変更することで強制的に新しいファイルをダウンロードさせることができます。

例えば、バージョン1のファイルが

<link rel="stylesheet" href="/css/site.css" />

だったとき、ファイルを更新したバージョン2は

<link rel="stylesheet" href="/css/site2.css" />

のようにすれば site2.css がクライアント側で利用されることになります。

しかしこれを手動で行うとなると、パスを変えたり物理ファイル名を変更したりと面倒でありミスも発生しやすくなります。

ファイルパスにクエリパラメータを付加する

Web の仕組みにより、パスの後ろにはキーと値を組み合わせたクエリパラメータを付加することができます。 クライアントのキャッシュの仕組みでは物理ファイルが同じ場合でもクエリパラメータが異なれば別ファイルとして認識されます。 クエリパラメータは付加しても利用目的がなければ何の意味も持たない値です。

クエリパラメータについては以下のような形式でパスに付加できます。

<!-- クエリパラメータなし -->
<link rel="stylesheet" href="/css/site.css" />

<!-- クエリパラメータあり -->
<link rel="stylesheet" href="/css/site.css?key=value" />

この仕組みを利用することにより、パスの変更は必要ですが、物理ファイルのファイル名を変更する必要はなくなります。

プログラムで自動的にクエリパラメータを付加する (悪い例)

このクエリパラメータをプログラムで自動付加することにより、 静的ファイルを変更した場合でもファイルパスを手動で変更する必要がなくなります。

簡単に思いつく例としてはユーザーが Web ページにアクセスしたときにクエリパラメータに現在時刻を付加する方法があります。 どのプログラミング言語でも1行で書けると思うので非常に簡単です。 生成例は以下のようになると思います。

<link rel="stylesheet" href="/css/site.css?20210223120000" />

しかしこれはキャッシュのメリットが一切なくなるというデメリットがあります。 Web ページにアクセスするたびにパスが変更されるので前回と同じファイルであるにも関わらず 別ファイルとして認識されるため、毎回サーバーから CSS ファイルがダウンロードされることになります。 古いバージョンのファイルを使用されることはなくなりますが、これは単純にキャッシュの機能を利用していないのと同じです。

ASP.NET Core におけるクエリパラメータ付加によるファイルバージョン管理

前置きがなくなりましたが、ASP.NET Core ではこのキャッシュの問題に対しての対応策が標準で備わっています。

以下のようにファイルパスを記述しているタグに対して asp-append-version="true" の属性を付加するだけです。

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
<img src="/image/sample.png" asp-append-version="true"/>
<script src="~/js/site.js" asp-append-version="true"></script>

これが Web に公開された後に実際にページにアクセスすると以下のように展開されます。

<link rel="stylesheet" href="/css/site.css?v=S2ihmzMFFc3FWmBWsR-NiddZWa8kbyaQYBx2FDkIoHs" />
<img src="/image/sample.png?v=Z0tUqQmLt_3L_NSzPmkbZKxL8cMxglf08BwWb5Od5z4" />
<script src="/js/site.js?v=dLGP40S79Xnx6GqUthRF6NWvjvhQ1nOvdVSwaNcgG18"></script>

ランダムな文字列が付加されますが、これはアクセスのたびに毎回変わるわけではありません。 この文字列はハッシュ値であり、参照しているファイルの中身を元に生成されます。 そのため、ファイルの内容が変わらない限りは同じ文字列が生成され、 ファイルの中身が変われば新しい文字列に変更されます。

これによりファイルが同じであればパスも同じであるためキャッシュされたファイルが優先的に使用されるようになり、 ファイルが更新されればパスが変わるので新しいファイルをダウンロードして使用されるようになります。

ちなみにこの asp-append-version="true" 属性は標準では wwwroot フォルダ内に配置した静的ファイルのみが対象となっています。 それ以外のファイルに設定してもランダム文字列は展開されないので注意して下さい。

サンプルプログラムについて

デフォルトのプロジェクトテンプレートでは site.js には asp-append-version が付加されていますが、 site.css には asp-append-version が付加されていません。

サンプルプログラムでは _Layout.cshtml ファイルの site.js, site.css にそれぞれ asp-append-version を付加しています。 また、参考として img タグを追加してそちらにも asp-append-version を付加しています。