Node.jsのビルドがOOMで落ちる?Ubuntuでのヒープ領域拡張とスワップ設定の手引き
Node.jsプロジェクトにおける「heap out of memory」問題とは何か
Node.jsによる静的サイト生成やバッチ処理が主流になるなか、JavaScript heap out of memory
というエラーはローカル・本番問わず一定数の開発者を悩ませています。
このエラーは一見「マシンのメモリが足りない」ように見えますが、実際には「Node.jsが使用可能なヒープ上限に達した」ために強制終了する現象です。
EleventyやNext.js、Astro、さらには独自CLIツールの構築など、Nodeベースのプロジェクトでは次のような状況でこの問題が発生します:
.md
や.njk
テンプレートを数百以上パースする- 巨大な検索インデックス(全記事パース)を一括生成する
fs
やglob
を用いた大規模I/O処理- 設定ミスにより無限ループ的にテンプレートを参照する
この問題は環境依存であるため、CI上や本番ビルド時にのみ顕在化することも多く、原因を正しく把握しづらい特徴があります。
なぜNode.jsはメモリを使い切るのか:仕組みの前提整理
Node.js(というよりV8エンジン)は、デフォルトで約1.4GB前後のヒープ領域しか確保しません。
node --v8-options | grep max_old_space_size
この値は、物理メモリの空き量にかかわらず固定であるため、マシンが8GB積んでいても、Node.jsが1.4GB以上使おうとするとクラッシュします。
特にEleventyなどテンプレートパースに依存するツールでは、全体メモリ使用量のほとんどがテンプレート読み込み・変換・パス生成の中間ステップに費やされ、ガベージコレクション前に限界に達することがあります。
ヒープ(Heap)領域とは、Node.jsなどの実行環境が確保するメモリ空間のうち、プログラム実行中に動的に割り当てる部分のこと。グローバル変数や長期間保持されるオブジェクトがここに格納される。
Ubuntuでのスワップ設定とヒープ拡張手順
本記事の事例では、以下の構成で問題が再現されました:
- 仮想サーバ:1.9GiB RAM、Swapなし
- Eleventy + 検索インデックス生成 + CSS/JS minify 処理
この構成で通常の npm run build
を行ったところ、下記のようなスタックトレースが発生。
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
このような状況で必要となるのは以下の2ステップです。
ステップ1:スワップ領域の作成(Ubuntu)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
スワップとは、物理メモリを超えてデータを一時的に退避させるディスク上の仮想メモリ空間のことです。これがないと、Node.jsのメモリ割当を拡張してもOSがカバーできません。
swapon --show
スワップ設定が有効かを確認します。
ステップ2:Node.jsのメモリ上限の拡張
NODE_OPTIONS="--max-old-space-size=3072" npx @11ty/eleventy
ここで重要なのは、Node.jsに明示的にヒープ拡張を許可しなければスワップを使わないという点です。実行時にこの環境変数がないと、2GBのスワップを用意しても全く意味をなしません。
"scripts": {
"build": "NODE_OPTIONS='--max-old-space-size=3072' npm run build",
"start": "NODE_OPTIONS='--max-old-space-size=3072' npx @11ty/eleventy --serve"
}
永続化したい場合は .bashrc
に以下を追記してもよいです。
export NODE_OPTIONS="--max-old-space-size=3072"
プロジェクト構造のレビュー観点
実装者はヒープやスワップの存在を前提とせずに構成するべきですが、レビューアーの立場では以下のような状況を設計ミスとして扱うべきです。
テンプレートやデータ読み込み処理で全ファイルを一括処理していないかを確認してください。メモリ効率より記述の単純化が優先されている場合、要見直しです。
@Reviewer: Eleventy等のフレームワークを使う場合、`_data/`配下のJSファイルが実質全テンプレートで評価されるため、不要な`fs.readdirSync()`や巨大JSON読込がヒープを圧迫していないか確認が必要です。
よくあるNG構成と改善例
// _data/articles.js
const fs = require("fs");
const path = require("path");
module.exports = () => {
const files = fs.readdirSync("./content/articles");
return files.map(f => JSON.parse(fs.readFileSync(`./content/articles/${f}`)));
};
@Reviewer:全ページでこれが評価され、毎回ファイルIOとメモリ割当が発生します。キャッシュ不可の書き方になっており、メモリが圧迫されます。再検討お願います。
module.exports = () => {
const glob = require("fast-glob");
return glob.sync("./content/articles/*.json").map((path) => ({ path }));
};
このように、構造と責務の分離がレビュー対象になります。情報の扱いが「全記事まとめ読み込み」になっている場合、検索用にまとめているのか、ページ生成にも使っているのか、責務を明示してください。
PlantUMLによる構成理解

上図のように、Node.jsのビルド処理が複数のユーティリティに分割されている場合、それぞれの処理単位でヒープ消費が異なります。レビューアーは「どの工程がメモリを使っているか」に注目してください。
設計観点まとめ:ヒープは「設計対象」である
Node.jsでのメモリ制限エラーは、単にメモリを増やせば良いという話ではありません。レビューアーとしては、次の観点で設計や実装をチェックすべきです。
- データアクセスの責務が広すぎないか
globalData
に意図しない重量データを含んでいないか- テンプレート間で冗長なincludeや再パースが発生していないか
map/filter
などで冗長なメモリ複製がないかNODE_OPTIONS
の指定が必要になっている構造がそもそも正しいか
ヒープ拡張が必要になった時点で「処理が大きすぎる」か「設計が粗い」かを疑うべき。スワップや環境変数は応急処置でしかない。
このような観点を踏まえ、Node.jsベースの静的サイトビルドやバッチ処理を安定化させるために、メモリ設計は優先事項として扱いましょう。