"use cache" はパラメータの扱いが賢い
さて、いくつかのパターンで"use cache"の使い方を解説してきましたが、軽く流してきた中に、非常に重要な点がありました。それは、"use cache"はパラメータの扱いが賢いという点です。
リスト4では、decodeBase64()関数の実行結果がパラメータごとに別々にキャッシュされていたことを思い出してみてください。開発者である私たちは、「base64strという引数をキーにしてキャッシュを管理してほしい」という指示をしていません。しかし、実際の挙動はそうなっています。
これはNext.jsがプロジェクトをコンパイルする際に、 "use cache"を指定した関数の引数を認識し、キャッシュのキーとして利用する形のコード生成を行なっているのです。これはコンポーネント関数についても同じで、"use cache"を指定したコンポーネントのpropsはキャッシュのキーとして利用されます。
リスト7で少し複雑なケースを見てみましょう。
function Profile({ id }) {
async function getNotifications(index, limit) { // (1)
"use cache";
return await db
.select()
.from(notifications)
.limit(limit)
.offset(index)
.where(eq(notifications.userId, id)); // (2)
}
return <User notifications={getNotifications} />;
}
このコードは、ProfileコンポーネントがUserコンポーネントを呼び出すケースです。UserコンポーネントはgetNotifications()関数をpropsとして受け取り、この関数を呼び出してデータベースから通知を取得します。(1)でgetNotifications()をクロージャとして実装しており、ここにだけ"use cache"を指定しています。
この場合、getNotifications()関数に渡すindexとlimitがキャッシュのキーとして利用されそうなことは想像がつきますね。
しかし、このデータベース呼び出しでは、Profileコンポーネントのidpropsも(2)のように利用されています。コンパイラはどのように扱うのでしょうか。
実は、コンパイラはかなり賢いようで、indexとlimitに加えてidもキャッシュのキーとして利用します。つまり、getNotifications()関数のキャッシュは、idとindexとlimitの3つの組み合わせが同じ場合だけ、同じキャッシュが使われるのです。
コンポーネントや関数がどんなパラメータに依存して処理結果(=キャッシュ対象のデータ)を生成するかは、コンパイラが自動で認識してくれるので、開発者はそのパラメータを意識しなくてもよいというわけです。素晴らしいですね。
なお、childrenをはじめとした一定以上複雑な(シリアライズできない)パラメータはキャッシュ対象にならないので、Static Rendering対象のコンポーネントの子にDynamic Rendering対象のコンポーネントを配置するような運用も可能です。
キャッシュを破棄する方法
最後に、キャッシュを破棄する方法を解説します。"use cache"を指定した関数やコンポーネントは、unstable_cacheLife()やunstable_cacheTag()といったAPIを利用してキャッシュの有効期間やキャッシュのタグを指定することで、キャッシュを破棄することができます(リスト8)。
import {
unstable_cacheTag as cacheTag,
unstable_cacheLife as cacheLife,
} from "next/cache";
export async function getDate1() {
"use cache";
const date = new Date();
cacheTag("date"); // (1)
return date.toISOString();
}
export async function getDate2() {
"use cache";
const date = new Date();
cacheLife("hours"); // (2)
return date.toISOString();
}
(1)のcacheTag()は、キャッシュのタグを指定するAPIです。cacheTag()に渡した文字列は、キャッシュのキーとして利用されます。従来のキャッシュ機構でfetch()にタグを付けていたのと同じような仕組みで、従来と同じようにServer Actions等でrevalidateTag()を呼び出すことでキャッシュを破棄することができます。
(2)のcacheLife()は、キャッシュの有効期間を指定するAPIです。引数の文字列に対応した時間をキャッシュのフレッシュな期間とし、その時間が経過すると、5分程度の猶予期間(stale時間)の間に古いキャッシュを返しつつ新しいキャッシュを構築します。HTTPヘッダーのCache-Controlにあるstale-while-revalidateと同じような仕組みですね。公式ドキュメントによれば、次のような種類があります。
-
default:15分(stale時間なし、破棄はしない) -
seconds:1秒(stale時間5分、1分で破棄) -
minutes:1分(stale時間5分、1時間で破棄) -
hours::1時間(stale時間5分、1日で破棄) -
days:1日(stale時間5分、1ヶ月で破棄) -
weeks:1週間(stale時間5分、1ヶ月で破棄) -
max:1ヶ月(stale時間5分、破棄はしない)
これらのAPIは、 "use cache"を指定した関数やコンポーネントの中で呼び出す必要があります。
まとめ
新しいキャッシュ機構は、後方互換を保ちつつも、従来のキャッシュ機構とはかなり違った発想で設計されていますが、基本的な方針は次の3つです。
-
Static Renderingにしたければ
"use cache"を指定する -
Static Rendering内のキャッシュを更新したければ
unstable_cacheLife()やunstable_cacheTag()で更新する -
Dynamic Renderingでオンデマンドに情報を出したければ
<Suspense>の中でPartial Prerenderingして、<Suspense>の外側がStatic Renderingの対象になるようにする
従来のキャッシュの方針と比べると、だいぶ意識する情報が減っているように思います。正式リリースはまだ少し先になるかもしれませんが、リリースされたらぜひ使ってみたいですね。
これからもNext.jsの進化に期待しつつ、本連載はここまでとします。ありがとうございました。
