HTTPヘッダーCache-Control stale-while-revalidateを理解する
こんにちは。タカギです。
先日は弊社のアイドルけいちゃんがNext.js12のMiddlewareについての記事を書いてくれました。
その記事からもわかるように、弊社でもフロントエンドにReactやNext.jsを採用することが増えてきています。
Next.jsを採用した場合、同じくVercelで開発されている SWR というReact Hooksライブラリを併せて使用することが多いのではないかと思います。
SWRの公式ドキュメントのトップページには以下の文章があります。
“SWR” という名前は、 HTTP RFC 5861 で提唱された HTTP キャッシュ無効化戦略である stale-while-revalidate に由来しています。 SWR は、まずキャッシュからデータを返し(stale)、次にフェッチリクエストを送り(revalidate)、最後に最新のデータを持ってくるという戦略です。
みなさん、stale-while-revalidateって知っていましたか?
お恥ずかしながら、私は知りませんでした。
今回の記事では、stale-while-revalidateの挙動を実際に試してみたいと思います。
※ この記事ではライブラリの方のSWRの解説はしません。
stale-while-revalidateとは
まずはMDN Web Docsを読みます。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control#stale-while-revalidate
レスポンスディレクティブの stale-while-revalidate は、キャッシュを再検証している間、古いレスポンスの再利用が可能なことを示します。
Cache-Control: max-age=604800, stale-while-revalidate=86400
上記の例では、レスポンスは 7 日間(604800 秒間)は新鮮です。7 日後、レスポンスは古くなりますが、キャッシュは翌日(86400 秒後)のリクエストに再利用できます。ただし、バックグラウンドでレスポンスを再検証することが条件です。
再検証により、キャッシュは再び新鮮になるため、クライアントはその期間中は常に新鮮であったかのように見えます。これにより再検証の遅延ペナルティを効果的にクライアントから隠蔽できます。
その間にリクエストがなければ、キャッシュは古くなり、次のリクエストは正常に再検証されます。
つまり図にするとこういうことです。 実際に検証してみましょう。
環境
- macOS Monterey 12.4
- PHP 8.1.4
- Slim 4.10.0
- Google Chrome 102.0.5005.61
実装
上述の通り、サーバー側の言語にはPHPを, フレームワークにSlimを使用します。
Slimをご存じない方は弊ブログで過去に紹介しておりますのでこちらの記事をご参考ください。
(と言っても、フレームワーク特有の機能はほぼ使いません。)
サーバーの現在日時を返す簡単なAPIを作成します。 先ほどの例では7日+1日としていましたが、流石にそんなには待てないので今回はmax-ageを30秒, stale-while-revalidateを10秒とします。
<?php
declare(strict_types=1);
namespace App\Http\Actions\Api;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class HelloAction
{
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
$json = json_encode([
'now' => $now
]);
$response->getBody()->write($json);
return $response
->withHeader('Content-Type', 'application/json')
->withHeader('Cache-Control', 'max-age=30, stale-while-revalidate=10');
}
}
このAPIをブラウザから叩けるようにします。
const sendButton = document.getElementById('send');
sendButton.addEventListener('click',async () => {
const requestedAt = new Date();
const response = await fetch('/api/hello')
.then(res => res.json());
console.log({
requestedAt: requestedAt,
serverTime: response.now,
})
});
画面はこの通りです。
Send Requestボタンを押すと、コンソールにフロント、サーバーそれぞれの現在日時が表示されます。
実施内容と結果を以下にまとめます。
ボタンクリック | リクエストしたフロント日時 | レスポンスのサーバー日時 | 備考 |
---|---|---|---|
1回目 | 00:59:41 | 00:59:41 | 初回なので当然キャッシュはない |
2回目 | 01:00:00 | 00:59:41 | 初回から30秒以内なのでキャッシュが使われた |
3回目 | 01:00:17 | 00:59:41 | 初回から30~40秒の間なのでキャッシュが使われた。バックグラウンドでリクエストが飛んでいる |
4回目 | 01:00:24 | 01:00:17 | 3回目にバックグラウンドで取得したキャッシュが使われた(3回目のリクエスト日時がレスポンスのサーバー日時と同じになっている) |
stale-while-revalidateの挙動が理解できましたね。
キャッシュが古くなった時一旦はそれをユーザーに見せながら裏で最新のデータを取得することで、サーバーへのリクエストにかかる時間をユーザーから隠蔽できるのが嬉しいポイントです。
ただ、stale-while-revalidateは2022年6月現在、全ての主要ブラウザが対応しているわけではないのでご注意ください。
参考:
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7
まとめ
- SWRの由来はHTTPヘッダーCache-Controlのstale-while-revalidateです
- stale-while-revalidateの挙動を実際のコードを動かして試しました
- ライブラリやフレームワークに触れる際、その背景や設計思想などを辿って理解を深めるのは良い習慣です!