条件付きGET
PHPは、動的にコンテンツを生成するために開発された言語であり、常に最新の情報を反映する必要があるため、ブラウザがサーバからの出力を常に取得するようになっています。
ただし、「条件付きGET」という仕組みを使用することで、PHPなどの動的に出力されるコンテンツでもブラウザのキャッシュを使うことができます。条件付きGETは、ブラウザがリクエストを送信する際に、サーバに対してコンテンツが更新されているかどうかを尋ね、更新されていなければブラウザが持つキャッシュを利用させる仕組みです。これにより、サーバの負荷をゼロ近くにまで軽減し、ページの読み込み速度を飛躍的に向上させることができます。
書いたコード
<?php
class CGET {
public function __construct($timestamp=0) {
if (!$timestamp) {
return;
}
$this->run($timestamp);
}
private function run($timestamp) {
$etag = sprintf('"%s"', md5($timestamp));
if (filter_input( INPUT_SERVER, 'HTTP_IF_NONE_MATCH') === $etag) {
$this->switchBrowserCache();
exit;
}
header('ETag: '.$etag);
$last_mod = gmdate('D, d M Y H:i:s T', $timestamp);
if (filter_input( INPUT_SERVER, 'HTTP_IF_MODIFIED_SINCE') === $last_mod) {
$this->switchBrowserCache();
exit;
}
header('Last-Modified: ' . $last_mod);
}
private function switchBrowserCache() {
header('HTTP', true, 304);
header('Content-Length: 0');
}
}
よく見かけるサンプルコードと基本的には同じです。
[2021/04/06追記] よく考えるとこのコードだと HTTP_IF_MODIFIED_SINCE の処理にリーチしません。
使い方
new CGET(filemtime(__FILE__));
readfile(__FILE__);
上記のように書くだけで動作します。
ポイントがいくつかあります。
ETagとは?
ETagは、コンテンツの同一性を識別するための仕組みです。サーバはコンテンツを生成するたびに、ETagと呼ばれる一意の識別子を付与し、ブラウザに返します。ブラウザは次回以降のリクエストに際して、If-None-Matchヘッダーを用いてサーバにETagを送信し、コンテンツが更新されていなければ、サーバはコンテンツを再度生成することなく、304 Not Modifiedレスポンスを返します。
一般的には、コンテンツを実際に生成して比較するのは負荷が大きくなるため、更新日時をETagの値として紐づけることが多くあります。この方法を採用することで、コンテンツが変更されたかどうかを迅速に判断することができます。
当記事でも、更新日時をETagの値として利用する簡易的な実装を採用しました。ただし、ETagは更新日時以外の値も使用することができます。例えば、コンテンツのハッシュ値やバージョン番号をETagに使用することもできます。
それぞれのリクエストヘッダに含まれる値について
ブラウザがリクエストを送信する際には、$_SERVER['HTTP_IF_MODIFIED_SINCE']
ヘッダーや $_SERVER['HTTP_IF_NONE_MATCH']
ヘッダーなどの「検証子」が付与されます。これらのヘッダーには、以前にサーバから返されたETagの値や、最終更新日時などが含まれています。ブラウザは、次回以降のリクエストに際して、これらの検証子を使用して、サーバ側にキャッシュの有効性を問い合わせます。
サーバは、検証子に基づいてクライアントのキャッシュの有効性を判断し、以下のいずれかのレスポンスを返します。
- 304 Not Modifiedレスポンス:サーバ側のコンテンツが変更されていない場合に返されます。この場合、サーバはクライアントに新しいコンテンツを送信する必要がないため、レスポンスボディは空のままです。
- 200 OKレスポンス:サーバ側のコンテンツが変更されている場合に返されます。この場合、サーバは新しいコンテンツをレスポンスボディに含めてクライアントに送信します。
これらの検証子を使用することで、ブラウザがキャッシュしたコンテンツを再利用しながらも、必要に応じて最新のコンテンツを取得することができます。
ETag判定だけでもよい
よく見かける条件付きGETのサンプルコードでは、一般的にETagヘッダとLast-Modifiedヘッダの両方の判定を行うことがありますが、ETagヘッダだけで条件付きGETを実現することができます。
ETagヘッダは、リソースの内容を識別するためのハッシュ値などの一意の値を含んでいます。ブラウザは、前回のレスポンスから取得したETag値をIf-None-Matchヘッダーで送信し、サーバは、そのETag値がリソースの最新バージョンと一致する場合には、304 Not Modifiedレスポンスを返します。つまり、サーバ側はETag値の一致のみで、リソースが更新されていないかどうかを判断することができます。
一方、Last-Modifiedヘッダーは、リソースの最終更新日時を含んでいます。ブラウザは、前回のレスポンスから取得したLast-Modified値をIf-Modified-Sinceヘッダーで送信し、サーバは、その値がリソースの最新バージョンよりも新しい場合には、304 Not Modifiedレスポンスを返します。ただし、時刻の精度が秒単位であるため、短時間の更新が反映されない可能性がある点に注意する必要があります。
つまり、ETagヘッダーだけでも条件付きGETを実現することができますし、一般的にはETagヘッダーの使用が推奨されています。
function cget($timestamp=0) {
if (!$timestamp) {
return;
}
$etag = sprintf('"%s"', md5($timestamp));
if (filter_input( INPUT_SERVER, 'HTTP_IF_NONE_MATCH') !== $etag) {
header('ETag: '.$etag);
return;
}
header('HTTP', true, 304);
header('Content-Length: 0');
exit;
}
上記のように書くだけで問題なく動作します。
以前は、ページの更新日時をLast-Modifiedヘッダーで判定し、古い場合には304 Not Modifiedレスポンスを返すのが一般的でした。しかし、PHPでサイトを構築する場合は、ページの中身が更新されていなくても、表示内容が異なることがあるため、日付判定だけでは正確な判断ができません。例えば、同じURLでも、パソコンとスマホで中身が異なる場合や、ブラウザの言語設定によって表示が変わる場合があります。
そのため、ETagヘッダーを使用して、ページの中身が更新されたかどうかを判定する方法が推奨されています。ETagヘッダーは、ページの中身が更新されるたびに変化する一意の識別子を含んでいます。ブラウザは、前回のレスポンスから取得したETag値をIf-None-Matchヘッダーで送信し、サーバは、そのETag値がリソースの最新バージョンと一致する場合には、304 Not Modifiedレスポンスを返します。このように、ETagヘッダーを使用することで、ページの中身が更新されていなくても、ブラウザ側でのキャッシュの再利用が可能となります。
ETagヘッダーは、ページの中身が更新されたかどうかを正確に判定できるため、ページのキャッシュに関する実装においては、Last-ModifiedヘッダーよりもETagヘッダーの使用が推奨されています。
Last-Modifiedは本当に不要?
過去には、「更新日時情報はSEO対策として重要」という都市伝説が広がり、Last-Modifiedヘッダーが有効であると考えられていました。そのため、実際にはあまり更新されていないにもかかわらず、ヘッダーの値を毎日のように更新しているサイトも多かったとされています。
しかし、Googleは更新日時情報を扱う際には非常に賢く、簡単に評価が上がることはないと考えられています。実際、日時情報を過剰に変更すると、その情報が信用されなくなる可能性があるため、過剰な更新は避けるべきです。また、作為的な更新は、Googleのアルゴリズムによって検出され、悪影響を与えることがあります。
したがって、日時情報を正確に伝えることは重要ですが、過剰な更新は避け、自然な形で情報が更新されるようにしましょう。また、そのような情報の更新によるSEOの影響も、過剰な期待はせず、コンテンツ自体の質の向上に努めることが大切です。
ETagの値はクオートで囲む
https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
ETagヘッダーの値は、HTTPの仕様によれば、必ずダブルクオートで囲む必要があります(RFC 2616に規定されています)。これは、ETag値が特定の文字列にマッチすることを防ぐために行われます。クオートで囲まれている場合は、ETag値の間にあるスペースや特殊文字が解釈されることがなく、正確な比較が可能になるためです。
ただし、PHPで出し分ける場合には、クオートで囲まなくても正常に動作することがあります。ただし、このような動作が許容されることは例外であり、正式なHTTPの仕様に従うことが望ましいです。
ETag: "xyzzy"
ETag: W/"xyzzy"
ETag: ""
ETagヘッダーの値は、空の値も可で、必ずしも全体を囲むわけではありません。
ETag値には、プレフィックス「W/」を付けることができます。これにより、ETag値がメタ情報であることを示し、クライアントがETag値を解釈する方法を調整することができます。これは「強いEtag・弱いEtag」ともいわれます。
ETagヘッダーの値は、サーバー側で自由に設定することができます。ETag値を用いたバージョン管理などに使用することができます。
2021/04/06 追記
https://developer.mozilla.org/ja/docs/Web/HTTP/Conditional_requests
ちゃんとした文書がありました。用語も正確です。