"use cache" の利用パターン
"use cache"は、宣言するスコープに応じて、少しずつ挙動が異なります。大きく分けて、次の3パターンがあります。
- コンポーネント関数をキャッシュ対象にする
- 通常の関数をキャッシュ対象にする
- ファイル全体をキャッシュ対象にする
ひとつずつ見ていきましょう。
1. コンポーネント関数をキャッシュ対象にする
まずはコンポーネント関数をキャッシュ対象にするパターンです(リスト3)。
export default function Home() { // (4)
return (
<div>
<main>
<Uuid />{/* (4) */}
</main>
</div>
);
}
async function Uuid() { // (1)
"use cache"; // (2)
// (3)
const data = await fetch("https://httpbin.org/uuid")
.then(res => res.json());
return (
<div>
<p>UUID: {data.uuid}</p>
</div>
)
}
(1)でUuidという名前の非同期コンポーネントを作成しました。このコンポーネントをキャッシュ対象にするために、関数の先頭に(2)のように"use cache"を宣言しています。ちなみに、"use cache"を指定できる対象は非同期関数だけで、同期関数に指定するとエラーになります。
このコンポーネントは内部で(3)のようにランダムなUUIDを取得するWeb APIを呼び出しており、キャッシュなしで動かす場合は実行するたびに違ったUUIDを取得・表示します。キャッシュが効いた場合は、UUIDが変わらないことを期待したいところです。では、実際に動かしてみましょう(図2)。
UUIDが表示されました。リロードしてみると、期待通り、UUIDが変わらないことがわかります。このパターンでは、<Uuid>コンポーネントが親である<Home>コンポーネントとまとめてStatic Renderingの対象になるため、ビルド時にキャッシュが生成されます。
2. 通常の関数をキャッシュ対象にする
次は、通常の関数をキャッシュ対象にするパターンです(リスト4)。
export default async function Home() {
console.log("Home"); // (5)
// (4)
const base64str1 = "aGVsbG8="; // hello
const base64str2 = "Z29vZGJ5ZQ=="; // goodbye
const text1 = await decodeBase64(base64str1);
const text2 = await decodeBase64(base64str2);
const text3 = await decodeBase64(base64str1);
const text4 = await decodeBase64(base64str2);
return (
<div>
<main>
<p>{base64str1} → {text1}</p>
<p>{base64str2} → {text2}</p>
<p>{base64str1} → {text3}</p>
<p>{base64str2} → {text4}</p>
</main>
</div>
);
}
// (1)
async function decodeBase64(base64str) {
"use cache"; // (2)
console.log("decodeBase64", base64str); // (6)
// (3)
const text = await fetch(`https://httpbin.org/base64/${base64str}`)
.then(res => res.text());
return text;
}
今度は、(1)で定義したdecodeBase64()関数をキャッシュ対象にするために、関数の先頭で(2)のように"use cache"を宣言しています。この関数は(3)でWeb APIを呼び出してBase64でエンコードされた文字列をデコードする機能があります。
この関数を、(4)のように<Home>コンポーネントの中で2種類のBase64文字列を2回ずつ呼び出してみました。Base64デコードの結果は何回やっても変わらないので、それぞれ2回目の呼び出し結果にはキャッシュが使われていると嬉しいところです。
では、実際に動かしてみましょう(図3)。
2回ずつ出力されていますが、これだけでは期待通りなのかどうか、まだわかりません。そこで、(5)と(6)に仕込んだconsole.log()の出力を見てみましょう(図4)。
1回のリロードで<Home>コンポーネントが3回評価されているので少し長めですが、まとまりとしては、Home関数が1回呼び出されるごとに、decodeBase64()関数が1回ずつだけ実行されていることがわかります。2回目の実行時にはキャッシュが使われるよう、コンパイル時に置き換えられるので、 decodeBase64()関数は呼び出されません。
また、引数ごとに別々のキャッシュが使われているのもうれしいポイントですね。
3. ファイル全体をキャッシュ対象にする
最後は、ファイル全体をキャッシュ対象にするパターンです(リスト5)。
"use cache"; // (1)
export async function getUuid() {
console.log("getUuid");
const data = await fetch("https://httpbin.org/uuid")
.then(res => res.json());
return data.uuid;
}
リスト3で使っていたUUID生成のWeb APIを関数として切り出して、別ファイルに定義しました。(1)でファイル自体に"use cache"を宣言しているので、このファイルからexportした関数はすべてキャッシュ対象になります。では、実際に画面に組み込んでみます(リスト6)。
import { getUuid } from "./uuid";
export default async function Home() {
console.log("Home");
const uuid1 = await getUuid();
const uuid2 = await getUuid();
return (
<div>
<main>
<p>{uuid1}</p>
<p>{uuid2}</p>
</main>
</div>
);
}
リスト4と同様に、複数回呼び出して様子を見ます。では、実行してみましょう(図5)。
同じ値が続いているので、キャッシュが効いていそうですね。ログも見てみましょう(図6)。
<Home>コンポーネントが3回評価されているのは図4と同様ですが、getUuid()関数は1回だけ呼び出されている点が異なります。これは、ビルド時にファイルごとにキャッシュが生成されているためです。
リスト4は、app/page.jsのリロードと同じタイミングでdecodeBase64()関数のキャッシュも破棄されていました。一方、リスト6のapp/uuid.jsは、開発サーバーの起動時のビルドで1回だけキャッシュが生成され、その後はapp/page.jsのリロードの影響を受けないため、キャッシュが破棄されることはありません。そのため、このような挙動になっています。これは"use cache"の指定先が関数かファイルかという違いではなく、コンポーネントとは別のファイルに関数を定義している効果ですね。
そしてこのケースでも、<Home>コンポーネント内はキャッシュされたコンテンツだけで構築されているので、ページ全体としてはStatic Renderingの対象になっています。あまり更新しなくていいコンテンツには"use cache"を指定して、最新情報を提供するコンテンツは<Suspense>内に置いてPartial Prerenderingを行う、といった使い分けができると、かなりスッキリしたコードになるのではないでしょうか。
