はじめに
Next.jsのバージョン14では非同期関数の結果をData Cache
に保存する関数としてunstable_cache
が用意されていました。
const data = unstable_cache(
// データを取り出す非同期関数
getData,
// キャッシュを識別するキー
['data'],
{
// キャッシュをまとめて無効にするデータのタグ
tags: ['user-data'],
// 与えられた秒数が経過した後のデータ取得後にデータを再検証する
// 未定義かfalseだとrevalidateTagやrevalidatePathをしない限りデータを再検証しない
revalidate: 3600,
},
)();
Next.jsのバージョン15ではunstable_cache
が廃止され、use cache
ディレクティブがその代わりを担うようになりました。
use cache
はどのような働きをしてくれるのでしょうか。この記事ではuse cache
の働きについて解説します。
Data Cache
Data Cache
はNext.jsが行う4つのキャッシュのうちの1つです。
他のキャッシュには、リクエスト単位に行われるRequest Memorization
、ビルド時に行われるFull Route Cache
、クライアント側で行われるClient-side Router Cache
があります。
Data Cache
は異なるリクエストやデプロイ間で共有される永続的なキャッシュです。説明ではfetch
とData Cache
についての関係について考えます。後ほどここで紹介したものとuse cache
とData Cache
との関係が対応します。
以下のようにfetch
でoptions.cache
をforce-cache
にしたデータ取得では、Data Cache
にデータがあればそのまま返し、なければ新たにデータを取得してData Cache
への保存とリクエスト先への返却を行います。
fetch(url, { cache: 'force-cache' });
Next.js15からはoptions.cache
を指定しない時のfetch
は他の非同期関数と同じように振る舞います。後述しますが、use cache
を使う世界では、fetch
のoptions.cache
によるキャッシュの管理は必要ないかも知れません。
再検証
キャッシュされたデータは2種類の方法でパージされます。
1つ目はfetch
でoptions.next.revalidate
を与えた時です。
fetch(url, { next: { revalidate: 60 } });
上記のようにoptions.next.revalidate
を60に設定した場合、最後にデータを取り出して60秒経過してから行われたデータ取得を最後にキャッシュが更新されます。
60秒経過するとキャッシュが自動的に削除されるのではなく、60秒経過後のデータの取得が終わったタイミングでData Cache
を更新することに注意が必要です(stale-while-revalidate
のような動きです)。
// 最初のデータ取得 データの返却+データをData Cacheに保存
fetch(url, { next: { revalidate: 60 } });
// 60秒以内のデータ取得 Data Cacheから結果を返す
fetch(url, { next: { revalidate: 60 } });
// さらに60秒以上経過してからデータ取得
// Data Cacheから結果が返される+Data Cacheを削除して新しいデータに更新
fetch(url, { next: { revalidate: 60 } });
// 1つ前のfetchで更新されたデータをData Cacheから返す
fetch(url, { next: { revalidate: 60 } });
2つ目はrevalidatePath
・revalidateTag
を用いた時です。
revalidatePath
はrevalidatePath('/dashboard')
のように引数に再検証させたいパスを渡して実行します。この操作は対象のパスで利用しているData Cache
を全て削除します。次のリクエストでデータの取得とData Cache
へのキャッシュが行われます。
revalidateTag
はrevalidateTag('user')
のように使います。引数に渡された文字列はタグで、そのタグを持つData Cache
を全て削除します。Data Cache
のタグはfetch
のoptions.next.tags
で渡せます。
fetch(url, { next: { tags: ['user', 'blog'] } });
キャッシュしない
fetch
の結果をキャッシュしたくない場合はoptions.cache
をno-store
にします。
fetch(url, { cache: 'no-store' });
このようにした場合、Next.jsはData Cache
を確認せずに常に新たに取得した結果を返しますし、その結果をData Cache
に保存しません。この場合もRequest Memorization
は行われることに注意してください。
options.cache
がデフォルトの場合とno-store
を指定した場合の違いはルートが動的になるかどうかにあります。no-store
の場合は動的ルートになり常にデータを取得しますが、デフォルトの場合は他の要素によって動的ルートにならない限りはFull Route Cache
を返すのでデータの取得を行いません。
use cache
fetch
と合わせてData Cache
がどのようなものかを紹介しました。
次に、use cache
を用いたData Cache
の取り扱いについて紹介します。
Next.jsの最新のバージョンが15.1の時にこの記事を作成しました。
use cache
はバージョン15.1にてcanary
とされています。動作の変更によってこの記事に誤った記述が生じることに注意してください。
use cache
を使う場合はまず、next.config.ts
でexperimental.dynamicIO
をtrue
にします。
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
...
experimental: {
...
dynamicIO: true,
},
}
export default nextConfig
dynamicIO
をture
にすると、use cache
のスコープにないすべての非同期関数を利用する箇所が事前レンダリングから除外されます。パフォーマンスの低下に気をつけましょう。
use cache
はuse server
と同じようにファイルの先頭やコンポーネント・関数の記述の先頭で宣言します。
use cache
が反映されたスコープは、すべての非同期関数がData Cache
とやりとりします(手動でオプトアウトしない限り)。つまり、そのスコープの非同期関数はすべてfetch
のoptions.cache
にforce-cache
を付与したときと同じような動きをします。
use cache
スコープ内で呼ばれた非同期関数は、その入力値等を元にキャッシュキーを自動で作成します。キャッシュキーを構成する値はシリアライズ可能なものだけで、シリアライズできない値はキャッシュキーに含まれないです。キャッシュヒットについての不具合等につながるので注意が必要です。
また、非同期関数の戻り値はシリアライズされてData Cache
へ保存されるのでシリアライズ可能な値である必要があります。
use cache
はファイルに対して、以下のように付与します。
'use cache';
export default async function Page() {
const data = await getData();
// ...
}
このように宣言したファイルにネストされた全てのコンポーネントはuse cache
を継承します。
use cache
を付与されたファイルのコンポーネントは、すべてのデータのやり取りをData Cache
を通じることを仮定できるのでSuspense
なしで非同期コンポーネントを配置できます。
逆にSuspense
はuse cache
の境界となり、Suspense
内のコンポーネントは動的にデータ取得を行います(children
も境界になります)。
// without-suspense/page.tsx
'use cache';
export default async function PageWithoutSuspense() {
return (
<div>
<UserData />
</div>
);
};
// with-suspense/page.tsx
'use cache';
export default async function PageWithSuspense() {
return (
<div>
<Suspense fallback={<UserDataFallback />}>
<UserData />
</Suspense>
</div>
);
};
// user-data.tsx
export const UserData = () => {
const user = await getUser();
return ...
}
上記の例のWithoutSuspensePage
ではUserData
のデータ取得でData Cache
へデータを問い合わせて取得するのに対して、WithSuspensePage
はUserData
のデータ取得で常に新しいデータを取得します。
コンポーネントは以下のように利用します。
export const CacheComponent = () => {
'use cache';
const data = await getData();
return ...
};
そのコンポーネント内で実行されるすべての非同期関数の結果がData Cache
に保存されます。異なる場所からこのコンポーネントを再利用する場合も、生成されるキャッシュキーに違いがない場合は同じキャッシュが利用されます。
関数は以下のように利用します。
export const getData = async () => {
'use cache';
const data = await fetch(url);
return data;
};
fetch
であればfetch(url, { cache: 'force-cache' })
のようにし、それ以外であればunstable_cache
を使うようにする必要がなくなり、同じuse cache
で管理できるようになりました。
再検証
Data Cache
で再検証を行う方法は2つありました。use cache
でもそれは同じです。
1つ目の再検証を行うまでの時間を定義する方法はcacheLife
を利用します。
cacheLife
はfetch
のoptions.next.revalidate
のように秒数を渡すのではなく、seconds
、minutes
、hours
、days
、weeks
、max
の決められた値を渡します。
それぞれの値は以下のように動きます。
value | Stale | Revalidate | Expire |
---|---|---|---|
デフォルト | undefined | 15分 | 無限 |
seconds | undefined | 1秒 | 1秒 |
minutes | 5分 | 1分 | 1時間 |
hours | 5分 | 1時間 | 1日 |
days | 5分 | 1日 | 1週間 |
weeks | 5分 | 1週間 | 1ヶ月 |
max | 5分 | 1ヶ月 | 無限 |
Stale
はクライアント側でデータをFresh
とみなす期間で、これを超えるとキャッシュを削除して次の実行からサーバーへデータの確認を行います。
Revalidate
はサーバーがデータのキャッシュの期限が切れていないことを確認する期間です。
Expire
はキャッシュが有効切れするまでの期間を指します。
2つ目の再検証であるrevalidatePath
、revalidateTag
で行う方法はそのまま使えます。
再検証の目印となるタグをはcacheTag
で付与します。
const getUsers = async () => {
'use cache';
const users = await getUsers();
cacheTag('users', 'user');
for (const user of users) {
cacheTag(`user-${user.id}`);
}
return users;
};
この場合getUser
はusers
、user
、user-id
・・・がタグとして紐づけられます。
キャッシュしない
非同期関数がなくても対象の場所で動的に実行させたい場合はconnection
を使います。
import { connection } from 'next/server'
export default async function Page() {
await connection()
...
}
connection
から先はすべてプレレンダリングから除外されます。これもcanary
の機能なので利用には気をつけましょう。
おわりに
Data Cache
とuse cache
について紹介しました。
Next.jsバージョン14の世界ではfetch
とその他の非同期関数でキャッシュする方法が異なり大変でしたが、use cache
の出現によりfetch
を他の非同期関数と同じように扱えるのでそれが解決されとても便利になりました。
これまでの動的ルートと静的ルートで物事を考えていると概念が混ざってわかりにくくなるので、別のモデルとして考えた方が理解しやすそうだなと思いました。