Node.jsは、シングルスレッドのイベントループモデルを採用しており、高い並行処理性能を持っています。
しかし、CPU負荷の高い処理や外部コマンドの実行が必要な場合、メインプロセスの外で子プロセスを生成することが一般的です。
子プロセスを生成するためのモジュールとして node:child_process が提供されていますが、その中でも exec と spawn の使い分けに迷うエンジニアは少なくありません。
これら二つの関数は、どちらも外部コマンドを実行するために使用されますが、データの処理方法やメモリの管理手法が根本的に異なります。
本記事では、2026年現在のNode.js環境における最新の知見を交えながら、これら二つのメソッドの違いを技術的な側面から詳しく解説します。
child_processモジュールの基本概念
Node.jsにおける子プロセス管理は、アプリケーションの安定性を左右する重要な要素です。
外部プログラムを実行する際、メインプロセスがブロックされるのを防ぐために非同期での処理が必要となります。
child_process モジュールには複数のメソッドが存在しますが、特に exec と spawn は最も頻繁に利用されます。
これらは一見似たような動作をしますが、「データを一括で受け取るか、ストリームとして受け取るか」という点が最大の違いです。
この違いを理解せずに実装すると、予期せぬメモリ不足によるクラッシュ(OOM: Out Of Memory)やパフォーマンスの低下を招く恐れがあります。
execメソッドの特徴と動作原理
exec メソッドは、シェルを介してコマンドを実行し、その結果をバッファとして一括で取得する手法です。
シェルを生成するため、パイプ | やリダイレクト > といったシェル特有の構文をそのまま記述できるという利便性があります。
execのメリット
もっとも大きなメリットは、その記述の簡潔さにあります。
実行結果をコールバック関数でまとめて受け取ることができるため、コードの見通しが良くなります。
設定情報の取得や、小さなテキストファイルの読み込みなど、出力データ量が事前に分かっている場合に適しています。
execのデメリットと制限事項
最大の懸念点は、実行結果をすべてメモリ上のバッファに格納するという点です。
デフォルトでは 200KB 程度のバッファ制限が設けられており、これを超える出力が発生するとエラーで停止します。
大規模なログの読み込みや、数MBを超えるようなバイナリデータの処理には向いていません。
execの使用例
const { exec } = require('node:child_process');
// 現在のディレクトリ内のファイル一覧を取得する
exec('ls -lh', (error, stdout, stderr) => {
if (error) {
console.error(`実行エラー: ${error.message}`);
return;
}
if (stderr) {
console.error(`標準エラー出力: ${stderr}`);
return;
}
// 全ての結果が一度にstdoutに格納される
console.log(`結果:\n${stdout}`);
});
結果:
total 8
-rw-r--r-- 1 user group 123B Jan 1 12:00 index.js
-rw-r--r-- 1 user group 456B Jan 1 12:00 package.json
spawnメソッドの特徴と動作原理
対照的に spawn メソッドは、コマンドを新しいプロセスとして起動し、データをストリーム(Stream)として処理します。
シェルを介さず直接バイナリを実行するため、オーバーヘッドが少なく、パフォーマンス面で非常に有利です。
spawnのメリット
データが生成されるたびに逐次処理を行うため、メモリ消費を極限まで抑えることが可能です。
数GBに及ぶような巨大なデータであっても、Node.jsのプロセスがメモリ不足で落ちることはありません。
また、リアルタイムで進行状況を表示したい場合や、長時間実行されるプロセス(サーバーの起動など)に非常に適しています。
spawnのデメリット
引数を配列として渡す必要があり、exec に比べるとコードがやや複雑になる傾向があります。
シェル構文を使いたい場合は、オプションで shell: true を指定する必要がありますが、セキュリティ面のリスクを考慮しなければなりません。
spawnの使用例
const { spawn } = require('node:child_process');
// lsコマンドを引数付きで実行
const ls = spawn('ls', ['-lh']);
// データが流れてくるたびに発生するイベント
ls.stdout.on('data', (data) => {
console.log(`ストリーム受信: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`エラー発生: ${data}`);
});
ls.on('close', (code) => {
console.log(`子プロセスが終了しました。終了コード: ${code}`);
});
ストリーム受信: total 8
-rw-r--r-- 1 user group 123B Jan 1 12:00 index.js
-rw-r--r-- 1 user group 456B Jan 1 12:00 package.json
子プロセスが終了しました。終了コード: 0
メモリ効率とパフォーマンスの徹底比較
これら二つのメソッドを選択する際の決定的な要因は、システムの「リソース効率」です。
以下の表で、主要な違いを整理します。
| 比較項目 | exec | spawn |
|---|---|---|
| データ転送方式 | バッファ(一括) | ストリーム(逐次) |
| メモリ使用量 | 出力サイズに比例して増加 | 常に一定で低負荷 |
| 最大データ量 | 制限あり(デフォルト 200KB) | 制限なし |
| 実行速度 | シェル起動の分、僅かに遅い | 高速 |
| 構文の簡便さ | 非常に簡単 | やや複雑 |
表から分かる通り、パフォーマンスと安定性を重視するなら spawn、記述の速さと小規模な出力を優先するなら exec を選ぶのが定石です。
2026年におけるベストプラクティス
Node.jsの開発環境も進化し続けており、現在は非同期処理(Async/Await)との親和性がより求められています。
本来の exec や spawn はコールバックベースですが、Promise化して扱うのが現代的な手法です。
util.promisify を活用した exec
util.promisify を使うことで、exec を直感的に Await できるようになります。
これにより、同期的なコードの流れを保ちつつ、非同期の外部コマンド実行を美しく記述できます。
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
async function getVersion() {
try {
const { stdout, stderr } = await exec('node -v');
console.log(`Node.js Version: ${stdout.trim()}`);
} catch (error) {
console.error(`エラー: ${error}`);
}
}
getVersion();
セキュリティ:シェルインジェクションの回避
exec はシェルを介するため、外部からの入力をそのままコマンドに渡すと、OSコマンドインジェクションの脆弱性を生む危険性があります。
例えば、ユーザーが入力したファイル名をそのまま exec(`rm ${filename}`) のように使用してはいけません。
セキュリティを担保するためには、引数を個別に指定できる spawn や execFile を優先的に検討すべきです。
どちらを使うべきかの判断基準
プロジェクトの実装において、どちらを選択すべきかの具体的なフローチャートを考えます。
execを選択すべきケース
- 実行するコマンドの出力が確実に小さく(数百KB以内)、メモリを圧迫しないことが保証されている。
- シェルのパイプ機能やワイルドカード展開を手軽に利用したい。
- スクリプトの実行結果を一度にまとめて文字列として処理したい。
spawnを選択すべきケース
- 動画変換(ffmpeg)や大量のログ解析のように、長時間実行され、連続的にデータが出力される。
- パフォーマンスを最大化し、メモリ消費を最小限に抑えたい。
- 子プロセスの標準入出力を、親プロセスの標準入出力にパイプ(接続)したい。
- セキュリティを重視し、シェルを介さずに引数を安全に渡したい。
実務での応用:spawnでの巨大ファイル処理
たとえば、数GBのログファイルから特定の文字列を検索して抽出するような場合、exec ではメモリ制限で即座に失敗します。
しかし spawn を使い、stdout を readline インターフェースに繋ぐことで、一行ずつ効率的に処理できます。
これは、クラウドネイティブな環境(AWS LambdaやGoogle Cloud Functionsなど)のように、利用可能なメモリが制限されている環境で特に威力を発揮します。
まとめ
Node.jsにおける exec と spawn の違いは、単なる記述の好みの問題ではなく、アプリケーションの堅牢性とスケーラビリティに関わる重要な設計判断です。
手軽に短時間のコマンドを実行したい場合は exec が適していますが、大規模なデータや継続的な出力を扱う場合は spawn が唯一の選択肢となります。
2026年現在のモダンな開発においては、原則として ストリーム処理が可能な spawn を優先的に検討し、安全で効率的な実装を心がけることを推奨します。
それぞれのメソッドの特性を正しく理解し、実行するコマンドの性質に応じて最適な手法を選択しましょう。
