Stale-while-revalidate(SWR)キャッシング手法は、Webアプリケーションのユーザにより迅速なフィードバックを提供すると同時に、結果整合性も可能にする。フィードバックを早くすることでスピナの表示が不要になり、より印象のよいユーザエクスペリエンスが得られる可能性がある。
Jeff Posnick氏はブログ記事で、stale-while-revalidate
キャッシュ手法の背景にある原理について説明している。
stale-while-revalidate
は、開発者が即時性 — キャッシュされたコンテンツを即時にロード — と鮮度 — 最新のキャッシュコンテンツが将来的に使用されることの保証 — のバランスを取る上で有効な手段です。
HTTPレスポンスヘッダのCache-Control
には、stale-while-revalidate
とmax-age
パラメータを設定することができる。HTTPリクエストに対するキャッシュ済のレスポンスがmax-age
(秒で表現される)より古い場合、その内容は"stale"であると判断される。staleレスポンスにおいて、キャッシュされたレスポンスの経過時間がstale-while-revalidate
設定でカバーされる時間ウィンドウ内であれば、staleレスポンスの返送とリクエストの再評価(revalidate)が並行して行われる。再評価リクエストに対するレスポンスは、キャッシュ内のstaleレスポンスと置き換えられる。キャッシュされたレスポンスがstale-while-revalidate
設定でカバーされる時間ウィンドウより古い場合には、ブラウザはネットワークから直接レスポンスを取得し、その応答がキャッシュに投入される。
stale-while-revalidateを使って自社の広告表示の速度とパフォーマンスを改善したGoogleは、次のように説明している。
これと同じテクニックは、スクリプトを可能な限り早くロードすることが最新のコードをロードすることよりも重要である場合など、他のシナリオにも適用することができます。
stale-while-revalidate
とmax-age
をCache-Control
レスポンスヘッダに設定する機能は、Chrome 75とFirefox 68でサポートされている。stale-while-revalidate
をサポートしていないブラウザは、この設定値を暗黙のまま無視して、max-age
を使用する。
動的APIを使用するシングルページアプリケーションでもstale-while-revalidate
手法を使用することは可能だ。この種のアプリケーションでは、アプリケーション状態の大部分がリモートに置かれたデータから("source of truth"として)取得されることが少なくない。そのようなリモートデータは他のアクタによって変更される可能性があるので、リクエスト毎にフェッチすれば常に最新のデータが返されることが保障される。stale-while-revalidate
手法は、"常に最新データを持つ"という要件を、"最終的に最新データを持てばよい"という要件に置き換えるものなので、
そのメカニズムはシングルページアプリケーションにおいても、HTTPリクエストと同じように機能する。アプリケーションがAPIサーバエンドポイントに最初のリクエストを送ると、その結果のレスポンスがキャッシュされ、返送される。次に同じリクエストを送信した時には、キャッシュされたレスポンスが即時に返送されると同時に、リクエストが非同期的に処理される。レスポンスが受信されると、キャッシュが更新され、UIにも適切な変更が行われるのだ。
stale-while-revalidate
手法は従って、ほとんどの場合においてユーザインターフェースの即時更新を可能にすると同時に、最新のレスポンスが利用可能になれば即時に表示されることで、最終的に表示データの正確性も実現することができる。
Next.js Reactフレームワークは、開発者にSWRフックを提供している。以下のコードはその使用例を示すものだ。
// Custom React hook using the useSWR hook to fetch remote data about a user
function useUser (id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error
}
}
// page component
function Page() {
return <div>
<Navbar />
<Content />
</div>
}
// child components
function Navbar () {
return <div>
...
<Avatar />
</div>
}
function Content () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <h1>Welcome back, {user.name}</h1>
}
function Avatar () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <img src={user.avatar} alt={user.name} />
}
上記のコードで示したuseUser
では、useSWR
フックを使用してリモートのユーザデータをフェッチしている。2つのコンポーネント(Content
とAvator
)がuseUser
フックをコールしているが、同じSWRキー(useUser
が使用する)を使用することでリクエストが自動的に重複排除され、キャッシュされ、共有されるため、APIに送信されるリクエストはひとつのみとなる。
useSWR
フックはさらに、コンポーネントがマウントされた時(revalidateOnMount
)、ウィンドウがフォーカスを獲得した時(revalidateOnFocus
)、ブラウザがネットワークに再接続した時(revalidateOnReconnect
)、定周期(refreshInterval
)、その他の設定時間(ドキュメント参照)にキャッシュを再評価するようにカスタマイズすることが可能である。Reactのリアクティブなバインディングによって、新たなレスポンスによってキャッシュが更新された時、UIがアップデートされて新しいデータが反映されることが保証される。
Tim Raderschad氏はSvelte Summit 2020で行った講演の中で、同様なAPIキャッシュ評価手法のSvelteでの実装について説明している。持前の簡潔性により、実装コードはSvelteのストアを活用したわずか20行に収められている。
// ./fetcher file
import { writable } from 'svelte/store'
const cache = new Map();
export function getData(url) {
const store = writable(new Promise(() => {}));
if(cache.has(url)){
store.set(Promise.resolve(cache.get(url)));
}
const load = async () => {
const response = await fetch(url);
const data = await response.json();
cache.set(url, data);
store.set(Promise.resolve(data));
}
load();
return store;
}
上記の関数を任意のコンポーネントでインポートすれば、データのフェッチとキャッシュが可能になる。
<script>
import Error from './Error.svelte'
import Spinner from './Spinner.svelte'
import { getData } from './fetcher'
const response = getData('https://0nzwp.sse.codesandbox.io/')
</script>
<h1>New Fetch</h1>
{#await $response}
<Spinner />
{:then data}
<code>{new Date(data).toTimeString()}</code>
{:catch}
<Error />
{/await}
ここでもSvelteのストアのバインディングによって、ストアの更新がユーザインターフェースに伝搬することが保証されている。
シングルページアプリケーションでSWRキャッシング手法が最も有効なのは、そのアプリケーションにとって古いデータを一時的に表示することが適切である場合であるという点に注意が必要だ。