【Next.js】４種類の"Cache"を理解したい Part ② Data Cache

App Routerの登場で、より強調された[**Caching**](https://nextjs.org/docs/app/building-your-application/caching)について理解を深めたいと思います。

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

1. [Request Memoization]
1. **Data Cache**
1. [Full Route Cache]
1. [Router Cache]

今回は[**Data Cache**](https://nextjs.org/docs/app/building-your-application/caching#data-cache)です。

:::note warn
V14までの仕様です。
V15からは、[Data Cache]と[Router Cache]がデフォルトで無効になります。
詳細は[こちら](https://qiita.com/Leech/items/c93ee489e12252908837)
:::

## 概要

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の動きのみに注目してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3468974/1abd2506-ccca-6925-2a1d-6cf891e6c49a.png)
出典 : [How the Data Cache Works]

1. Rendering時に、Fetchが発生した場合、Request MemoizationとData Cacheを順にチェックし、Cacheが無ければ、Data Sourceに対してRequestがそのまま行われる
1. Responseは、Request MemoizationとData Cacheに保存されつつ、Request元に戻される
1. 次のFetchに `{ cache: 'no-stoire' }`が含まれている場合は、Data Cache層でのCacheはスキップされる。ちなみに、Request MemoizationはOptionが異なるため、新規Reuqestとして認識され、Cacheを返していない


### 保持期間

明示的にRevalidationやCache無効化を行わない限り、永続的に保持されます。


### Revalidating

２種類の方法があります。

- Time-based Revalidation

`fetch('https://...', { next: { revalidate: 3600 } })`のように、時間（秒）を指定することで、時間経過後にRevalidationをおこなってくれます。

ここでポイントとなるのは、一定時間経過後に、まず**Data Cacheに現存するStale Cacheを返したあと**に、Revalidationが行われることです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3468974/7f3add39-dc03-67ca-4b60-94996c124094.png)
出典 : [How Time-based Revalidation Works]

- On-demand Revalidation

`revalidateTag('tag-name')`や`revalidatePath('/path-name')`を実行します。

前者は、指定されたTagのData CacheをPurge（削除）し、後者は指定されたPathのRe-Renderingをに行いつつ、Data CacheのRevalidationをおこなってくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3468974/8c643f5a-39d6-997d-4107-eee5759ca018.png)
出典 : [How On-Demand Revalidation Works]

### Cache無効化

- Fetchごとに個別に無効化する場合

```js
fetch(`https://...`, { cache: 'no-store' })
```

- [Route Segment Config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config)で無効化する場合

```js
export const dynamic = 'force-dynamic'
```

## 実際に確認

Buttonアクションで、`/api`配下のRouteにRequestを飛ばし、そこから外部APIサーバーにRequestを送るというサンプルを作成します。外部APIサーバーはランダム値を返すので、Cacheが利用されていない場合、Responseは全て異なることになります。

外部APIサーバー向けのRequestがData Cacheに保持されます。


### 実装

##### Project

```bash:shell
npx create-next-app@latest --typescript --tailwind
```

##### Component

```tsx:src/app/data-cache/page.tsx
'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

```typescript:src/utils/app-fetch.ts
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

```typescript:src/app/api/data-cache/normal.ts
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 });
}
```

```typescript:src/app/api/data-cache/time-based-revalidation.ts
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 });
}
```

```typescript:src/app/api/data-cache/on-demand-revalidation.ts
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 });
}
```

```typescript:src/app/api/data-cache/opt-out.ts
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サーバー

[こちら](https://qiita.com/Leech/items/b118ae11d348ca37c271#%E3%83%80%E3%83%9F%E3%83%BC%E5%A4%96%E9%83%A8api%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC)と同様に`extenral-server.js`ファイルを作成してください。


### 外部API、Next.Jsサーバーを起動

```bash:shell
node external-server.js
```

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

```bash:shell
npm run dev
```

起動後、`http://localhost:3000/data-cache`にアクセスします。

### 再起動とCacheクリア

`.nect/cache/fetch-cache`に、Data Cacheが保持されるので、**検証の度にfetch-cacheフォルダを手動で削除し、`npm run dev`を実行してください。**



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3468974/2cbfc0b6-bde7-6967-145e-6b478b352ed5.png)



### 通常のData Cache

`Normal`ボタンを押下してください。

3回のRequest全てに同じ値が返ってきており、Cacheが利用されていることが分かります。

```bash:Next.Jsサーバーログ
Normal 1
130
Normal 2
130
Normal 3
130
```

### Time-based Revalidation

[再起動とCacheクリア](#再起動とcacheクリア)をします。

`Time-based revalidation`ボタンを押下してください。

１つ目のRequestでData Cacheが生成され、２、３つ目のRequestに対してCacheが返されています。


ただし、３つ目に関しては、`revalidate : 5`で設定した５秒が過ぎているので、Stale Cacheが返されており、裏でRevalidationが動き、Cacheが更新されます。

４つ目は、上記で更新されたCacheから値が返されるため、新しい値となっています。

```bash:Next.Jsサーバーログ
Time-based revalidation 1
611
Time-based revalidation 2
611
Time-based revalidation 3
611
Time-based revalidation 4
826
```

### On-Demand Revalidation

[再起動とCacheクリア](#再起動とcacheクリア)をします。

`On-demand revalidation`ボタンを押下してください。

１つ目のRequestで生成されたCacheが２つ目に利用されていることが分かります。

その後、`revalidateTag('leech')`により、Cacheが削除されるため、３つ目のRequestは外部APIサーバーから新たに値を取得しています。

```bash:Next.Jsサーバーログ
On-demand revalidation 1
123
On-demand revalidation 2
123
On-demand revalidation 3
114
```

### Cacheを無効化

[再起動とCacheクリア](#再起動とcacheクリア)をします。

`Opt-out`ボタンを押下してください。

Data Cacheを無効化しているため、全てのRequestが外部APIサーバーから値を取得しています。

```bash:Next.Jsサーバーログ
Opt-out data cache 1
664
Opt-out data cache 2
91
Opt-out data cache 3
249
```

## 最後に

Request Memoizationとの組み合わせで、最適な動きが実現されそうな気がしつつも、仕組みを知らないことで調査が難航するなんてこともありそうです。

ちなみに、今回初めて知ったのですが、Route HandlerもStatic Renderingの対象なんですね。奥が深いです。

[Request Memoization]: https://qiita.com/Leech/items/b118ae11d348ca37c271
[Full Route Cache]: https://qiita.com/Leech/items/405b0641d9d30f55a51e
[Router Cache]: https://qiita.com/Leech/items/c9669e7d109651d1106c

[How the Data Cache Works]: https://nextjs.org/docs/app/building-your-application/caching#data-cache

[How Time-based Revalidation Works]: https://nextjs.org/docs/app/building-your-application/caching#time-based-revalidation

[How On-Demand Revalidation Works]: https://nextjs.org/docs/app/building-your-application/caching#on-demand-revalidation