Nuxt3 SSR 時のメモリ枯渇対策

# はじめに
[株式会社よりそう](https://qiita.com/organizations/yoriso) では一部のサービスで SSR モードの Nuxt でコンテンツを配信しています。
ある時 Nuxt をホスティングしている環境でメモリ使用量が増加し、メモリが枯渇することで、配信に影響を与えてしまっていました。
調査した結果、メモリ使用量の増加は複数の要因によるものでした。
本記事ではメモリリークの解消とメモリ使用量抑制ために実際に行った対策についてまとめます。

# 調査方法
## プロセスのメモリ使用状況を定期的に吐き出させる方法
1. nuxt アプリをビルドする
1. ビルド時に生成される `.output/server/index.mjs` に下記のコードを追加する
    ```js
    // メモリ使用状況をログに出力する関数
    function logMemoryUsage() {
      const used = process.memoryUsage();
      const messages = [];
      for (let key in used) {
        messages.push(`${key}: ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`);
      }
      console.log(new Date().toISOString(), 'Memory Usage:', messages.join(', '));
    }
    
    // 1秒ごとにメモリ使用状況をログに出力
    setInterval(logMemoryUsage, 1000);
    ```
1. `node .output/server/index.mjs` でビルドしたサーバを起動
1. 定期的に負荷をかける (実際には 1 秒ごとに異なるページにアクセスするツールを作成して利用しました)
1. ログに表示されるメモリ増加量を観察する。heapUsed が著しく増加しないかを見る。
    ```
    2024-04-18T11:02:23.330Z Memory Usage: rss: 47.55 MB, heapTotal: 8.5 MB, heapUsed: 6.8 MB, external: 1.92 MB, arrayBuffers: 0.02 MB
    2024-04-18T11:02:24.331Z Memory Usage: rss: 47.91 MB, heapTotal: 8.76 MB, heapUsed: 7.06 MB, external: 1.92 MB, arrayBuffers: 0.02 MB
    2024-04-18T11:02:25.331Z Memory Usage: rss: 47.92 MB, heapTotal: 8.76 MB, heapUsed: 7.06 MB, external: 1.92 MB, arrayBuffers: 0.02 MB
    2024-04-18T11:02:26.333Z Memory Usage: rss: 47.92 MB, heapTotal: 8.76 MB, heapUsed: 7.07 MB, external: 1.92 MB, arrayBuffers: 0.02 MB
    2024-04-18T11:02:27.336Z Memory Usage: rss: 47.96 MB, heapTotal: 8.76 MB, heapUsed: 7.07 MB, external: 1.92 MB, arrayBuffers: 0.02 MB
    ...
    ```
## メモリインスペクタを使う方法
[こちらの記事](https://tech.andpad.co.jp/entry/2020/03/19/080036)を参考にして、Chrome のメモリインスペクタを使って調査をしました。

https://tech.andpad.co.jp/entry/2020/03/19/080036

1. nuxt アプリをビルドする
1. `node --inspect .output/server/index.mjs` を実行
1. 1 分程度負荷をかける (ツールを使い 1 秒ごとに異なるページにアクセス)
1. Chrome のメモリインスペクタの画面でメモリの使用状況を確認する

この手順で、Pinia がメモリリークに関連していそうだとあたりをつけることができました。

# 対策方法
## Nuxt をバージョンアップする
Nuxt の 3.11.0 より前のバージョンでは、SSR 時にメモリリークが発生するようです。

https://github.com/nuxt/nuxt/issues/25362

当時最新版の 3.11.2 にアップデートしたところ、メモリリークの挙動が緩和されました。

## Pinia で外部ファイルで定義した参照を state に入れない
やや入り組んでいるのですが、
- サーバ側で読み込まれる middleware 内の処理で
- 外部ファイルで定義しているオブジェクトの参照を
- pinia の state に代入すると

メモリリークのような挙動になることが分かりました。

`structuredClone` などで値をコピーすることで回避することができます。

```data.ts
export const initialData = {
  sample: value
}
```

```dataStore.ts
import { initialData } from "./data.ts"

export const useDataStore = defineStore('data', {
  state() {
    return { data: null }
  },
  actions: {
    // middleware 内で呼び出している処理
    initData() {
      // NG: 外部ファイルで定義しているオブジェクトの参照を代入する
      // this.data = initialData
      
      // OK: structuredCloneを使用
      this.data = structuredClone(initialData)
    }
  }
});
```

## Nitro のキャッシュ設定を変更する
Nuxt サーバでレンダリングした結果は、サーバのメモリにキャッシュされます。
我々は CDN でのキャッシュを行なっているので、メモリでのキャッシュは不要でした。

ただ、ドキュメントなどを読み漁りましたが、レンダリング結果のキャッシュを無効にする方法が見つかりませんでした。
そこで driver に、デフォルトよりもより細かくオプション設定が可能な lruCache を設定し、max を 1 にして、実質的にビルドキャッシュをさせないようにしています。

苦肉の策ですが、これでメモリ使用量をかなり抑制することができました。

```nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'aws-lambda',
    serveStatic: false,
    storage: {
      cache: {
        driver: 'lruCache',
        max: 1,  // キャッシュサイズを最小限に設定
      },
    },
    devStorage: {
      cache: {
        driver: 'lruCache',
        max: 1,  // 開発時も同様に設定
      },
    },
  },
});
```

# まとめ
メモリ使用量の増加の原因は下記でした。
- Nuxt のバージョン
- 外部ファイルに定義したオブジェクトの参照を Pinia の State に代入していたこと
- Nitro のキャッシュ設定

複合的な原因があり、エラーも吐かないので調査には時間がかかりましたが、メモリの使用状況を可視化し、地道に観察することで改善することができました。

この記事がどなたかのメモリ使用状況の改善につながると幸いです。