App Routerの登場で、より強調されたCachingについて理解を深めたいと思います。
ドキュメントに沿って、4回に分けて記事にしていきます。
- Request Memoization
- Data Cache
- Full Route Cache
- Router Cache
今回はData Cacheです。
V14までの仕様です。
V15からは、[Data Cache]とRouter Cacheがデフォルトで無効になります。
詳細はこちら
概要
ServerサイドにおけるHttp Requestの結果を永続的にCacheします。
.next/cache
配下にCacheが保持されることで永続化を可能としています。
Next.Jsが従来のServerサイド(Node.Js)のFetchを拡張することで、このData Cacheを実現しています。BrowserサイドにおけるFetchは、Browserが保持するCacheとやり取りするものなので混乱しないように注意しましょう。
※永続化に関して、CDNやEdge NetworkなどのCache Serverも保存場所となるはずですが、ドキュメントのみでは詳細が分かりませんでした。
Request Memoizationとの違い
-
保持期間
Request MemoizationはRendaringごとに保持されるため、永続的ではありません。 -
保存場所
Rendaring Serverに、Cacheが保持されます。
例えば、Data CacheがCache Server(CDNやEdge)に配置されている場合、そこへのRequest自体を削減する目的があります。今回のサンプルのように、Rendering ServerとCache Serverが同じ場合は大きな違いは出ないですね。
How it works
Rendering時は、Request Memoizationも同時に動きますが、ここではData Cacheの動きのみに注目してください。
- Rendering時に、Fetchが発生した場合、Request MemoizationとData Cacheを順にチェックし、Cacheが無ければ、Data Sourceに対してRequestがそのまま行われる
- Responseは、Request MemoizationとData Cacheに保存されつつ、Request元に戻される
- 次のFetchに
{ cache: 'no-stoire' }
が含まれている場合は、Data Cache層でのCacheはスキップされる。ちなみに、Request MemoizationはOptionが異なるため、新規Reuqestとして認識され、Cacheを返していない
保持期間
明示的にRevalidationやCache無効化を行わない限り、永続的に保持されます。
Revalidating
2種類の方法があります。
- Time-based Revalidation
fetch('https://...', { next: { revalidate: 3600 } })
のように、時間(秒)を指定することで、時間経過後にRevalidationをおこなってくれます。
ここでポイントとなるのは、一定時間経過後に、まずData Cacheに現存するStale Cacheを返したあとに、Revalidationが行われることです。
出典 : How Time-based Revalidation Works
- On-demand Revalidation
revalidateTag('tag-name')
やrevalidatePath('/path-name')
を実行します。
前者は、指定されたTagのData CacheをPurge(削除)し、後者は指定されたPathのRe-Renderingをに行いつつ、Data CacheのRevalidationをおこなってくれます。
出典 : How On-Demand Revalidation Works
Cache無効化
- Fetchごとに個別に無効化する場合
fetch(`https://...`, { cache: 'no-store' })
- Route Segment Configで無効化する場合
export const dynamic = 'force-dynamic'
実際に確認
Buttonアクションで、/api
配下のRouteにRequestを飛ばし、そこから外部APIサーバーにRequestを送るというサンプルを作成します。外部APIサーバーはランダム値を返すので、Cacheが利用されていない場合、Responseは全て異なることになります。
外部APIサーバー向けのRequestがData Cacheに保持されます。
実装
Project
npx create-next-app@latest --typescript --tailwind
Component
'use client';
export default function Home() {
const onNormal = async () => {
await fetch('/api/data-cache/normal');
};
const onTimeBased = async () => {
await fetch('/api/data-cache/time-based-revalidation');
};
const onDemand = async () => {
await fetch('/api/data-cache/on-demand-revalidation');
};
const onOptOut = async () => {
await fetch('/api/data-cache/opt-out');
};
return (
<div>
<button onClick={onNormal} className="border bg-yellow-200 mt-3 mx-3 p-2">
Normal
</button>
<button onClick={onTimeBased} className="border bg-blue-200 mx-3 p-2">
Time-based revalidation
</button>
<button onClick={onDemand} className="border bg-green-200 mx-3 p-2">
On-demand revalidation
</button>
<button onClick={onOptOut} className="border bg-red-200 mx-3 p-2">
Opt-out
</button>
</div>
);
}
Fetch Function
const { signal } = new AbortController();
export async function getRandomNumForDataCache(next?: NextFetchRequestConfig) {
// Opt-out Request Memoization
const res = await fetch('http://localhost:3005', { signal, next });
console.log(await res.text());
}
Route Handler
import { getRandomNumForDataCache } from '@/utils/app-fetch';
export async function GET() {
for (let index = 1; index < 4; index++) {
console.log(`Normal ${index}`);
await getRandomNumForDataCache();
}
return new Response('', { status: 200 });
}
import { getRandomNumForDataCache } from '@/utils/app-fetch';
export async function GET() {
console.log('Time-based revalidation 1');
await getRandomNumForDataCache({ revalidate: 5 });
// This will hit cache
await new Promise<void>((resolve) =>
setTimeout(async () => {
console.log('Time-based revalidation 2');
await getRandomNumForDataCache();
resolve();
}, 2000)
);
// This will hit stale cache
await new Promise<void>((resolve) =>
setTimeout(async () => {
console.log('Time-based revalidation 3');
// This request torrigers revalidation so it includes the revalidate option
await getRandomNumForDataCache({ revalidate: 5 });
resolve();
}, 5000)
);
// This will hit cache revalidated by the above request
await new Promise<void>((resolve) =>
setTimeout(async () => {
console.log('Time-based revalidation 4');
await getRandomNumForDataCache();
resolve();
}, 1000)
);
return new Response('Time-based revalidation', { status: 200 });
}
import { getRandomNumForDataCache } from '@/utils/app-fetch';
import { revalidateTag } from 'next/cache';
export async function GET() {
console.log('On-demand revalidation 1');
await getRandomNumForDataCache({ tags: ['leech'] });
console.log('On-demand revalidation 2');
await getRandomNumForDataCache();
revalidateTag('leech');
console.log('On-demand revalidation 3');
await getRandomNumForDataCache();
return new Response('', { status: 200 });
}
export async function GET() {
console.log('Opt-out data cache 1');
await optOut();
console.log('Opt-out data cache 2');
await optOut();
console.log('Opt-out data cache 3');
await optOut();
return new Response('', { status: 200 });
}
const optOut = async () => {
const res = await fetch('http://localhost:3005', { cache: 'no-store' });
console.log(await res.text());
};
ダミー外部APIサーバー
こちらと同様にextenral-server.js
ファイルを作成してください。
外部API、Next.Jsサーバーを起動
node external-server.js
上記の外部APIサーバーを起動したShellとは別プロセスで起動してください。
npm run dev
起動後、http://localhost:3000/data-cache
にアクセスします。
再起動とCacheクリア
.nect/cache/fetch-cache
に、Data Cacheが保持されるので、検証の度にfetch-cacheフォルダを手動で削除し、npm run dev
を実行してください。
通常のData Cache
Normal
ボタンを押下してください。
3回のRequest全てに同じ値が返ってきており、Cacheが利用されていることが分かります。
Normal 1
130
Normal 2
130
Normal 3
130
Time-based Revalidation
再起動とCacheクリアをします。
Time-based revalidation
ボタンを押下してください。
1つ目のRequestでData Cacheが生成され、2、3つ目のRequestに対してCacheが返されています。
ただし、3つ目に関しては、revalidate : 5
で設定した5秒が過ぎているので、Stale Cacheが返されており、裏でRevalidationが動き、Cacheが更新されます。
4つ目は、上記で更新されたCacheから値が返されるため、新しい値となっています。
Time-based revalidation 1
611
Time-based revalidation 2
611
Time-based revalidation 3
611
Time-based revalidation 4
826
On-Demand Revalidation
再起動とCacheクリアをします。
On-demand revalidation
ボタンを押下してください。
1つ目のRequestで生成されたCacheが2つ目に利用されていることが分かります。
その後、revalidateTag('leech')
により、Cacheが削除されるため、3つ目のRequestは外部APIサーバーから新たに値を取得しています。
On-demand revalidation 1
123
On-demand revalidation 2
123
On-demand revalidation 3
114
Cacheを無効化
再起動とCacheクリアをします。
Opt-out
ボタンを押下してください。
Data Cacheを無効化しているため、全てのRequestが外部APIサーバーから値を取得しています。
Opt-out data cache 1
664
Opt-out data cache 2
91
Opt-out data cache 3
249
最後に
Request Memoizationとの組み合わせで、最適な動きが実現されそうな気がしつつも、仕組みを知らないことで調査が難航するなんてこともありそうです。
ちなみに、今回初めて知ったのですが、Route HandlerもStatic Renderingの対象なんですね。奥が深いです。