1月4日、Next.jsは新たなキャッシュモデル「'use cache'」のコンセプトを発表した。このモデルは、アプリケーションのパフォーマンス向上と開発者体験の向上を目的としており、キャッシュロジックを簡素化しつつ高い柔軟性を提供するものである。
本記事は、以下のエキスパートに監修していただきました:
監修者からの補足:
Next.js開発チームはキャッシュに関して様々な方法をこれまでも模索していました。過去には、 fetch() 関数を使うとNext.jsはデフォルトでキャッシュするという仕組みを採用していましたが、その動きはわかりにくいため、キャッシュのデフォルトはオフになりました。それでは逆にキャッシュしたい場合はどうすると良いのか、このキャッシュに関する仕組みを Next.jsチームはずっと検討しています。
Next.js開発チームは今回発表された"use cache"ディレクティブを今後採用しようとしています。とはいえ、現時点では experimental な機能なのでこれで確定というわけではありません。
ディレクティブなので、ファイル単位、コンポーネント単位、関数単位のそれぞれで付与することができ、付与されるとそれぞれの単位で引数などの依存情報を基にキャッシュキーを作ってキャッシュしてくれます。
これにより、ファイル全体から関数単体まで、様々な単位でのキャッシュが可能になる上、宣言的な方法で自動的に引数を見たうえでキャッシュを作ることができるようになります。手続き的な方法で手動でキャッシュキーを引用しながらキャッシュを作るよりも効率的になります。
昨年10月に公開されたOur Journey with Cachingという記事には、use cache が生み出された背景が記されていますので、合わせて参照すると良いでしょう。
'use cache'とは何か
'use cache'
は、必要に応じてデータやコンポーネントをキャッシュし、アプリケーションの高速化を図るJavaScriptディレクティブである。コードに文字列リテラルとして挿入し、Next.jsコンパイラに特定の「境界」を示す。たとえば、サーバーからクライアントへの遷移を指示する。
このアプローチはReactの'use client'
や'use server'
と似ており、フレームワークがコードの実行場所を最適化して管理するために利用される。
動作の仕組み
以下の簡単な例を見てみよう。
async function getUser(id) {
'use cache';
let res = await fetch(`https://api.vercel.app/user/${id}`);
return res.json();
}
このコードは、'use cache'
ディレクティブによってサーバー関数として変換される。この過程でキャッシュエントリの「依存関係」が検出され、キャッシュキーの一部として利用される。たとえば、id
がキャッシュキーに含まれる。getUser(1)
を複数回呼び出すと、キャッシュされた結果が返される。
さらに高度な例として、サーバーコンポーネントのキャッシュを扱う次のコードを見てみよう。
function Profile({ id }) {
async function getNotifications(index, limit) {
'use cache';
return await db
.select()
.from(notifications)
.limit(limit)
.offset(index)
.where(eq(notifications.userId, id));
}
return <User notifications={getNotifications} />;
}
この例では、index
やlimit
だけでなく、親コンポーネントから渡されるid
もキャッシュキーに含まれる。このように、'use cache'
は依存関係を自動的に検出し、キャッシュキーに組み込むことでキャッシュの誤りを防ぐ。
なぜ関数ではなくディレクティブを選ぶのか
キャッシュロジックを関数で書く場合、開発者がキャッシュキーを手動で指定する必要がある。例えば以下のコードを考える。
function Profile({ id }) {
async function getNotifications(index, limit) {
return await cache(async () => {
return await db
.select()
.from(notifications)
.limit(limit)
.offset(index)
.where(eq(notifications.userId, id));
});
}
return <User notifications={getNotifications} />;
}
このコードでは、id
がキャッシュキーに含まれていないため、キャッシュの競合や古いデータのリスクがある。一方、'use cache'
ディレクティブは、コンパイラがコードを静的解析し、すべての依存関係を確実に処理する。
非シリアル化入力値の取り扱い
キャッシュ対象の入力値には、以下の2種類がある。
- シリアル化可能:
JSON.stringify
で安定した文字列形式に変換可能な値。Reactの直列化機能を利用して、Promiseや循環構造を含む複雑なオブジェクトも処理可能。 - 非シリアル化可能: キャッシュキーに 含まれず 、サーバーリファレンスとして扱われる。
以下の例では、children
の変更がキャッシュを無効化しない理由を示している。
async function Profile({ id, children }) {
'use cache';
const user = await getUser(id);
return (
<>
<h1>{user.name}</h1>
{children}
</>
);
}
この仕組みは、キャッシュキーがid
に基づいて生成される一方で、children
はリファレンスとして扱われるためである。
タグ付けと無効化
キャッシュのライフサイクルは、cacheLife関数で制御できる。また、cacheTagを用いて特定のキャッシュエントリをタグ付けし、revalidateTag()
で無効化することも可能である。
async function getPost(postId) {
'use cache';
let res = await fetch(`https://api.vercel.app/blog/${postId}`);
let data = await res.json();
cacheTag(postId, data.authorId);
return data;
}
シンプルかつ強力なキャッシュモデル
'use cache'
は、次の特徴を持つ。
- シンプル: ローカルな理由付けでキャッシュエントリを作成可能。グローバルな副作用を気にする必要がない。
- 強力: 実行時に変化する値もキャッシュ可能。
この機能は現在実験的であり、早期のフィードバックが求められている。
詳細はComposable Caching with Next.jsを参照していただきたい。