cacheSignal:React Server Componentsにおける非同期処理のキャンセル制御
cacheSignalは、React Server Components(RSC)でcacheと組み合わせて使うことで不要な非同期処理やリソース消費を抑制する機能です。なぜこういった仕組みが必要になったのでしょうか。
まず、RSCでは、サーバー側で非同期処理(例:DBクエリ、API実行など)を行いながらレスポンスを構築します。このとき、cacheを使うことで同じ引数の関数呼び出し(例えば同じURLなど)をキャッシュして、処理が重複して走らないように制御できます(De-duplication)。
ただし、そのキャッシュの寿命を知る手段がないと、ある非同期処理が途中で不要になった場合(レンダリング中止やユーザー離脱など)でも、無駄にその処理が走り続けてしまう可能性があります。
このためのキャンセル制御を提供するのがcacheSignalで、「もう結果を使わなくなった(描画が完了した/中止された/失敗した)ため途中処理を中断可能である」ことを検知できるようになります。
cacheSignalの仕様
cacheSignalはレンダリング時に呼ばれるとAbortSignal(キャンセル可能なシグナル)を返します。このシグナルは以下のタイミングでabort(キャンセル済み状態)になります。
- Reactがそのキャッシュ対象の描画を正常に完了したとき
- レンダリングが中止されたとき
- レンダリングが失敗したとき
よって、このシグナルをcacheの対象関数(fetchなど)に渡すことで「もうこの結果を使わなくなる」というタイミングで処理を中断できるようになります。
cacheSignalの具体例
以下はcacheSignalの使用例です。
import { cache, cacheSignal } from "react";
const cachedFetch = cache(async (url: string) => {
const response = await fetch(url, { signal: cacheSignal() });
return await response.json();
});
export async function CachedComponent({ page }: { page: string }) {
// page は動作確認用の識別子
let data = null;
try {
data = await cachedFetch("http://localhost:3000/api/heavy");
} catch (err: any) {
if (err.name === "AbortError") {
console.log("Fetch aborted: ", page);
// キャンセルされて発生した例外は無視する
return null;
}
// 本来のエラーなら再throwやロギングなど
console.error("Fetch error: ", page);
throw err;
}
return <div>{data?.message}</div>;
}
この例では、複数の場所からcachedFetchに同じurlのデータを要求してもcacheの仕組みによって同じ結果を共有できます。かつ、もしReact側でそのレンダリングが中止されたり、描画が完了した場合は、cacheSignal()の返すAbortSignalがabortされ、fetchがキャンセルされます(fetchは仕様上AbortSignalに対応しているため、signalオプションとして指定することでキャンセルが可能です)。
また、ここではcatchによるエラーハンドリングで「キャンセルによるエラー」を無視/分離するように書いています。AbortSignalによるキャンセルはエラーとして扱いたくない場合が多いと思うので、こういった実装が基本となるでしょう。
実際にサンプルプログラムを実行し、Next.jsの実行ログを眺めてみましょう。まず、page.tsxにはCachedComponentが2つ配置されていますが、両方とも同じAPIエンドポイントを呼び出しているため、fetchは1回だけ実行されています。cacheの効果で、同じURLに対して不要なfetchが走らないことが確認できます(図1-①)。
次に、page1ロード中に[Go to page2]リンクをクリックすると、page2のサーバー側でpage2のAPI実行が開始されますが、フロントエンド側はpage1のロードが完了するまで待機します(図1-②、③)。
page1のAPI実行が終わりレンダリングが完了すると、実行ログにFetch aborted: page2のログが出力され、page2側のfetchがキャンセルされていることが確認できます(図1-④)。
cacheSignalのユースケースと注意点
例えば、大きなファイルをダウンロードするAPIや重い計算処理、サブスクライブ系の非同期処理などがある場合、cacheSignalをcacheと組み合わせて使うことで、結果が不要になった時点でキャンセルさせ、リソースの節約につなげられます。
ただし、cacheSignalはレンダー関数内で呼ぶ場合に限り有効です。それ以外ではnullを返す(つまりキャンセルできない)よう設計されています。また、現時点ではRSC専用として実装されているため、クライアントコンポーネントでは常にnullを返します(将来、クライアントキャッシュ更新の際に使われる可能性もあるようです)。
