App Routerの登場で、より強調されたCachingについて理解を深めたいと思います。
ドキュメントに沿って、4回に分けて記事にしていきます。
- Request Memoization
- Data Cache
- Full Route Cache
- Router Cache
今回はRequest Memoizationです。
V14までの仕様です。
V15からは、Data CacheとRouter Cacheがデフォルトで無効になります。
詳細はこちら
概要
Component Treeから発生する同じURL・Optionを持つRequestとその結果のResponseをメモリにCacheします。
つまり、Rendering時に発生する重複したRequestに対して、Cacheを保持してくれます。
出典 : Next.Js Request Memoization Overview
複数のComponentから同じリクエストが発生した場合も、Request Memoizationが集約しResponseをCacheしてくれています。
以下のように、わざわざComponentの上位層でData Fetchして、Propsで下位層に渡すということをしなくてよくなります。
export default async function ParentComponent(){
const result = await fetchSomething();
// わざわざPropsで渡さなくてよい!
// ChildComponent内で await fetchSomethig()を行う。
return (
<div>
<ChildComponet result={result} />
</div>
)
}
How it works
出典 : How Request Memoization Works
- 最初のRequestは、メモリに存在しないので、Cacheは取れない
- そのため、Data SourceへのRequestが実行され、結果がCacheに格納される
- 以降に発生する一連のRendaring内での同じRequestは、Cacheから結果が取得される
- 一連のRendaringが完了し、Routeが完成したタイミングで、メモリのCacheはクリアされる
ポイント
- Request Memoization機能は、Reactの機能だが、Next.JsのCacheメカニズムとも連携するため、4種類のCacheに含まれている
-
fetch
を用いた、Get
Requestのみ対応 - Server Componentの
fetch
Requestが対象 - Route Handler内の
fetch
は対象外。なぜなら、Component Treeに含まれていない、Rendering外の部分のため
Server ComponentをRenderingするタイミングで、効果を発揮するCacheということですね。
保持期間
Rendering中のみ保持されます。
例えば、Buildを2回行った場合、それぞれRendaringが別なので、1回目のCacheが2回目のRendaring時に使用されることはありません。
Revalidating
RendaringごとにCacheが生成され、上記で記載したように他のRendaringには影響しません。
そのため、Revalidating処理は存在しません。
Cache無効化
以下の方法で、無効化します。
const { signal } = new AbortController()
fetch(url, { signal })
実際に確認
Rendering時に確認できるCacheなので、next build
やnext start
の後のページリクエスト時のRequest Memoization Cacheを確認するサンプルを作成します。
実装
Project
npx create-next-app@latest --typescript --tailwind
Component
import Component1 from '@/components/component1';
import Component2 from '@/components/component2';
import Component3 from '@/components/component3';
// Static Rendaing
export default function Home() {
console.log('Home');
return (
<div>
<Component1 />
<Component2 />
<Component3 />
</div>
);
}
import { getRandomNumber } from '@/utils/app-fetch';
// Static Rendaing
export default async function Component1() {
getRandomNumber();
console.log('Component1');
return <div>Component1</div>;
}
import { getRandomNumber } from '@/utils/app-fetch';
import Component4 from './component4';
// Static Rendaing
export default async function Component2() {
getRandomNumber();
console.log('Component2');
return (
<div>
<div>Component2</div>
<Component4 />
</div>
);
}
import { getRandomNumber } from '@/utils/app-fetch';
import { cookies } from 'next/headers';
// Dynamic Rendaing
export default async function Component3() {
// Dynamic Functions
cookies();
getRandomNumber();
console.log('Component3');
return <div>Component3</div>;
}
import { getRandomNumber } from '@/utils/app-fetch';
import { cookies } from 'next/headers';
// Dynamic Rendaing
export default async function Component4() {
// Dynamic Functions
cookies();
getRandomNumber();
console.log('Component4');
return <div>Component4</div>;
}
Fetch Function
export async function getRandomNumber() {
// 次回説明するData Cacheの影響を抑えるためのOptionを追加している。
const res = await fetch('http://localhost:3005', { cache: 'no-store' });
console.log(await res.text());
}
ダミー外部APIサーバー
Root配下に作成します。
const http = require('http');
const app = http.createServer((req, res) => {
// Make random numbers
const random = (Math.random() * 1000).toFixed(0);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(random);
});
app.listen(3005, '127.0.0.1');
console.log('Extenral Server is running on port 3005');
外部APIサーバーを起動
※状況に応じて再起動をしてください。
node external-server.js
Static Rendaring
Next.Jsアプリケーションをビルドします。
上記の外部APIサーバーを起動したShellとは別プロセスで起動してください。
npm run build
Next.Jsサーバーのログに、Static RendaringされたComponentのログが出ています。
外部APIサーバーに対するRequest(getRandomNumber()
)も実行されており、同じ値が返ってきているのでCacheが効いていますね!
Home
Component1
Component2
875
875
Dynamic Rendaring
ビルドしたアプリを起動し、http://localhost:3000/request-memoization
にBrowserでアクセスします。
npm run start
Dynamic Server Componentが存在するRouteは、Route全体がDynamic Renderingになるため、/request-memoization
配下の全てのComponentがページリクエスト時に動的に生成されます。そのため、全てのComponentからログが排出されています。
こちらもgetRandomNumber()
の結果が全て同じなので、Cacheが効いていることがわかります。
Home
Component1
Component2
Component3
Component4
917
917
917
917
Cacheを無効化
試しに、Request Memoization機能を無効にしてみます。
+const { signal } = new AbortController();
export async function getRandomNumber() {
- const res = await fetch('http://localhost:3005', { cache: 'no-store' });
+ const res = await fetch('http://localhost:3005', { cache: 'no-store', signal });
console.log(await res.text());
}
npm run build
、npm run start
で確認してみると、getRandomNumber()
からRequestごとに異なる値が返ってきています。
Home
Component1
Component2
Component3
Component4
935
308
746
860
npm run dev
で起動したときは・・・
http://localhost:3000/request-memoization
にアクセスした初回はCacheが期待通り動くのですが、2回目以降はCacheが効いていないです。
この不安定な動きに関しては、Issueが既に上がっているようなので、何かしらのアクションが起こるはず。
最後に
Dynamic Renderingが1つでも発生すると、そのRouteのServer Componentは全てDynamic Renderingされるということを今回初めて知りました。
Cacheだけでなく、全体的な理解も求められてきますね!