Next.js 15のデフォルトcache指定が変わったので実測してみた

## 概要

Next.js の v15 がリリースされバージョンアップされる方も多いかと思います。
そのまま上げてもなんとなく動作してしまいますが、cache 周りの概念が大きく変わっているので足元すくわれないようにするため v15 と以前のバージョンでの cache の挙動の違いを実測を交えて説明したいと思います。

## 対象ユーザー

- Next.js を利用していて v15 にアップデートを検討されている方
- Next.js の fetch の cache について興味がある方

## Next.js v15 の変更点

[公式 Doc](https://nextjs.org/blog/next-15)にもありますが大きく変わったのは以下の 2 点かと思います。

#### [API の非同期化](https://nextjs.org/blog/next-15#async-request-apis-breaking-change)
  次の`cookies`、`headers`などが非同期化されました。利点としてはこれらの API からのデータ取得が非同期化されるためレンダリングがブロックされずにより高速に処理されます。
  つまりこれらの API の結果に左右されないコンポーネントは ASI レスポンスを待たずに先にレンダリングされるようになります。
  ※ 非同期化は大きな変更なので v15 では従来の同期 API も並行で利用できるようです（次のメジャーバージョンアップまで対応する旨の警告が出ます）

#### [デフォルトのキャッシュルールが「あり」→「なし」に変更](https://nextjs.org/blog/next-15#caching-semantics) ← 今回はこの話
  これは結構影響力がある変更だと思います、そのままバージョンアップしても特に問題無く表示されるためうっかり見逃してしまう可能性がありそうです。
  この変更点を理解して正しいキャッシュ設定に変更しないと fetch リクエスト先のサーバーに多大な負荷をかけてしまい、下手するとサーバーダウンにつながる可能性が出てしまいます。

## Next.js AppRouter での fetch 仕様確認

おさらいで Next.js AppRouter での ServerComponent レンダリング時の fetch の動作を確認してみます。
[公式 Doc](https://nextjs.org/docs/app/building-your-application/caching#request-memoization)にあるように複数のコンポーネントでそれぞれ fetch 処理を行っても 1 リクエスト時の重複分は Next.js 側がまとめて処理を行い同じリクエストがされないようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316898/fd49b933-4ee2-3ef6-3119-77387b3375b4.png)

これは React コンポーネント指向によくマッチしていてコンポーネント間の依存関係を薄くして実装できるメリットがあります。

### キャッシュがデフォルト状態で v15 にアップデートした場合の課題

v15 ではキャッシュのデフォルトが「なし」になったためデフォルト状態のままアップデートすると以下の図の All Requests の状態で API にリクエストされてしまい、
場合によっては 1 リクエストあたり数十（場合によっては数百）の API リクエストがされ API サーバーが高負荷状態になる恐れがあります。

以前の動作を期待するには fetch の option でキャッシュ指定を`force-cache`に設定する必要があります。

公式サンプル

```ts
async function getPost(id: string) {
  const res = await fetch(`https://api.vercel.app/blog/${id}`, {
    cache: "force-cache",
  });
  const post: Post = await res.json();
  if (!post) notFound();
  return post;
}
```

## 実際の fetch リクエストを計測する

v15 アップデート時に cache option を`force-cache`指定にして一件落着としたいのですが、本当にキャッシュが有効になっているか心配ですよね。
設定ミスをしていた場合でも普通にページが表示されますが、ある日突然 API サーバーが高負荷でダウンしたり、従量課金 API サービスから高額な請求が届いてやっと異変に気付くなどは避けたいものです。

### 計測の課題

Next.js の Memoized Request 機能は内部処理のため実際に有効になっているかの計測が難しいです。
検証用の API サーバーを立てて Next.js と 1 対 1 の状態にしてサーバーログを確認するなども出来ますが、サーバーが他のリクエストも受け付けていたりするとログから検証時のリクエストを抽出するのが大変です。
また API サービスを利用している場合はそもそもサーバーログの閲覧が出来ない場合も考えられます。

### 計測方法

今回はクライアント側で通信状態を確認するためパケットキャプチャを行い、キャッシュ有り無しで API サーバーにリクエスト状態の計測行います。

計測方法は以下の環境とツールで行いました。

- 作業環境： macOS
- 計測ツール： tcpdump （OS 標準ツール）
- 解析ツール： Wireshark

tcpdump で計測するにあたり他の通信がノイズとして乗らないように送信先の API サーバーのみに絞るようにします。

1. API サーバーの IP アドレスを調べる
   例）

```sh
$ nslookup www.sample.com

Name:	www.sample.com
Address: 5.22.145.121
Name:	www.sample.com
Address: 5.22.145.16
```

2. 取得した IP アドレスを向き先ホストに指定してキャプチャを開始します。
   キャプチャ結果を Wireshark で解析できるように`-w`オプションでファイル保存されるようにしています。
   計測完了したら`Control + c`でキャプチャを終了します。

   例）

```sh
$ tcpdump -s 0 -w ~/capture.log dst host 5.22.145.121 or dst host 5.22.145.16
```

3. Wireshark で閲覧する
   tcpdump で保存したファイルを Wireshark で開きメニューの `Statics`->`Protocol Herarchy`からトラフィック状態を計測

### 計測結果

#### v14 `cache: 指定なし`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316898/4b3e4d29-b577-c3e9-416c-67d8e87ab686.png)

#### v14 `cache: no-store`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316898/875fcea2-540a-69a9-a9cf-12bc41450192.png)

#### v15 `cache: 指定なし`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316898/6b7ef696-63ff-7842-0cee-64c9d20a5847.png)

#### v15 `cache: force-cache`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316898/718abc45-329d-1b8d-df24-14499db948fa.png)


## まとめ

計測結果より cache 指定なしの状態で Next.js を v14 から v15 にアップデートすると通信量の増加から cache 無効状態となっていることが推察されました。
従来通りの動きを期待するのであればcache指定を`force-cache`指定にする必要がありそうです。

推測するな計測せよという有名な言葉がありますが、指定した設定が思惑通り動作しているかは実際に測ってみることが重要で是非習慣化しておきたいと感じました。