9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js】4種類の"Cache"を理解したい Part ① Request Memoization

Last updated at Posted at 2024-03-04

App Routerの登場で、より強調されたCachingについて理解を深めたいと思います。

ドキュメントに沿って、4回に分けて記事にしていきます。

  1. Request Memoization
  2. Data Cache
  3. Full Route Cache
  4. Router Cache

今回はRequest Memoizationです。

V14までの仕様です。
V15からは、Data CacheRouter Cacheがデフォルトで無効になります。
詳細はこちら

概要

Component Treeから発生する同じURL・Optionを持つRequestとその結果のResponseをメモリにCacheします。

つまり、Rendering時に発生する重複したRequestに対して、Cacheを保持してくれます。

image.png
出典 : Next.Js Request Memoization Overview

複数のComponentから同じリクエストが発生した場合も、Request Memoizationが集約しResponseをCacheしてくれています。

以下のように、わざわざComponentの上位層でData Fetchして、Propsで下位層に渡すということをしなくてよくなります。

parent-component.jsx
export default async function ParentComponent(){
    const result = await fetchSomething();

    // わざわざPropsで渡さなくてよい!
    // ChildComponent内で await fetchSomethig()を行う。
    return (
    <div>
        <ChildComponet result={result} />
    </div>
    )
}

How it works

image.png
出典 : How Request Memoization Works

  1. 最初のRequestは、メモリに存在しないので、Cacheは取れない
  2. そのため、Data SourceへのRequestが実行され、結果がCacheに格納される
  3. 以降に発生する一連のRendaring内での同じRequestは、Cacheから結果が取得される
  4. 一連の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無効化

以下の方法で、無効化します。

sample.js
const { signal } = new AbortController()
fetch(url, { signal })

実際に確認

Rendering時に確認できるCacheなので、next buildnext startの後のページリクエスト時のRequest Memoization Cacheを確認するサンプルを作成します。

実装

Project
shell
npx create-next-app@latest --typescript --tailwind
Component
src/app/request-memoization/page.tsx
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>
  );
}
src/components/component1.tsx
import { getRandomNumber } from '@/utils/app-fetch';

// Static Rendaing
export default async function Component1() {
  getRandomNumber();
  console.log('Component1');
  return <div>Component1</div>;
}
src/components/component2.tsx
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>
  );
}
src/components/component3.tsx
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>;
}
src/components/component4.tsx
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
src/utils/app-fetch.ts
export async function getRandomNumber() {
  // 次回説明するData Cacheの影響を抑えるためのOptionを追加している。
  const res = await fetch('http://localhost:3005', { cache: 'no-store' });
  console.log(await res.text());
}
ダミー外部APIサーバー

Root配下に作成します。

external-server.js
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サーバーを起動

※状況に応じて再起動をしてください。

shell
node external-server.js

Static Rendaring

Next.Jsアプリケーションをビルドします。

上記の外部APIサーバーを起動したShellとは別プロセスで起動してください。

shell
npm run build

Next.Jsサーバーのログに、Static RendaringされたComponentのログが出ています。

外部APIサーバーに対するRequest(getRandomNumber())も実行されており、同じ値が返ってきているのでCacheが効いていますね!

Next.Jsサーバーログ
Home
Component1
Component2
875
875

Dynamic Rendaring

ビルドしたアプリを起動し、http://localhost:3000/request-memoizationにBrowserでアクセスします。

shell
npm run start

Dynamic Server Componentが存在するRouteは、Route全体がDynamic Renderingになるため、/request-memoization配下の全てのComponentがページリクエスト時に動的に生成されます。そのため、全てのComponentからログが排出されています。

こちらもgetRandomNumber()の結果が全て同じなので、Cacheが効いていることがわかります。

Next.Jsサーバーログ
Home
Component1
Component2
Component3
Component4
917
917
917
917

Cacheを無効化

試しに、Request Memoization機能を無効にしてみます。

src/utils/app-fetch.ts
+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 buildnpm run startで確認してみると、getRandomNumber()からRequestごとに異なる値が返ってきています。

Next.Jsサーバーログ
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だけでなく、全体的な理解も求められてきますね!

9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?