Node.js で「JavaScript heap out of memory」が発生する
V8 のヒープ上限(既定 1.5〜4GB 程度)を超えるとプロセスが落ちる。
--max-old-space-size でヒープを増やすか、ストリーム化 / リーク修正で使用量を減らす。
CI ビルドや巨大 JSON の処理で頻発。
#node#memory#heap#v8#build
要約
FATAL ERROR: ... JavaScript heap out of memory は V8 の old space が上限に達した とき出る。
短期的には --max-old-space-size で上限を引き上げ、根本対処として ストリーム化 と リークの特定 を行う。
CI でだけ発生する場合は実行マシンの空きメモリ自体も疑う。
よくある原因
- 上限不足: ビルドや巨大 JSON の処理で、V8 既定のヒープ上限を超える
- 一括展開:
fs.readFileSyncで巨大ファイル全体を Buffer に乗せている、JSON.parseで 100MB の配列を一気にパースしている - リーク: モジュールスコープの Map に push しっぱなし、
removeListenerし忘れ、setIntervalをclearIntervalしていない - ビルドツールのピーク: webpack / vite / tsc の型チェック・ソースマップ生成のピークがマシンを超える
解決策
1. ヒープ上限を上げる
# 一回限り
node --max-old-space-size=4096 ./script.js
# 環境変数で常時
export NODE_OPTIONS="--max-old-space-size=4096"
npm run build公式 CLI ドキュメント の通り単位は MB。
物理メモリの 70〜80% を上限の目安にする。
2. ストリーム化する
import fs from "node:fs";
import readline from "node:readline";
const rl = readline.createInterface({
input: fs.createReadStream("./huge.jsonl"),
crlfDelay: Infinity,
});
for await (const line of rl) {
const row = JSON.parse(line);
process(row);
}JSON Lines や CSV はストリームと相性が良く、メモリ消費は定数化する。
3. ヒープスナップショットでリーク特定
node --inspect ./server.js
# Chrome DevTools → Memory → Take heap snapshotHeap Snapshot 公式ガイド の手順で 2 回スナップショットを取り、Comparison で増え続けているオブジェクトを見つける。
リスナや Map の参照を疑う。
4. ビルドツール側で抑える
- webpack:
cache: { type: "filesystem" }、devtool: falseで sourcemap を消す - vite:
build.sourcemap: false、build.rollupOptions.output.manualChunksで分割 - tsc:
--incremental+--buildで差分化、skipLibCheck: trueで型チェック節約
CI なら NODE_OPTIONS=--max-old-space-size=4096 を env に入れるのが手っ取り早い。