Node.jsは非同期で高性能なアプリケーション開発に適していますが、運用中に「メモリ使用量がじわじわと増加する」「一定時間後にサーバーが落ちる」といった現象が発生することがあります。こうした現象の多くは、メモリリークに起因します。
この記事では、Node.jsにおけるメモリリークの典型的な原因と、それを検出・解析するためのツールとしてheapdumpやChrome DevToolsのprofilerを使った実践的な対処法を紹介します。
Node.jsでよくあるメモリリークの原因
メモリリークとは、不要になったオブジェクトがガベージコレクションされずにメモリ上に残り続ける現象です。以下のようなコードパターンで発生します。
1. グローバル変数やクロージャによる参照の残存
let cache = [];
function fetchData() {
cache.push(new Array(1000000).fill('data')); // 毎回追加され、解放されない
}
2. イベントリスナーの登録解除漏れ
function setup(ws) {
ws.on('message', handleMessage); // 接続が切れてもリスナーが残るとリーク
}
3. マップや配列に不要なデータを保持し続ける
const sessionStore = new Map();
function handleRequest(userId) {
sessionStore.set(userId, { timestamp: Date.now() });
}
上記のように、意図しない参照の保持や、明示的な削除忘れがメモリリークの主因です。
heapdumpを使ってヒープスナップショットを取得
heapdump
は、実行中のNode.jsプロセスのヒープ(メモリ領域)の状態をファイルに出力できるパッケージです。
インストール
npm install heapdump
スナップショットの出力コード
const heapdump = require('heapdump');
const express = require('express');
const app = express();
app.get('/snapshot', (req, res) => {
const filename = `heap-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, (err, filename) => {
res.send(`Heap snapshot written to ${filename}`);
});
});
app.listen(3000);
上記のようにエンドポイントを作成することで、任意のタイミングでスナップショットを取得できます。
Chrome DevToolsでスナップショットを可視化
取得した.heapsnapshot
ファイルは、ChromeのDevToolsから読み込むことで解析可能です。
- Chromeを開く
- 開発者ツール →
Memory
タブ - 「Load」ボタンからスナップショットを読み込み
「Retainers」や「Object Types」を見ながら、どのオブジェクトが不要に残っているかを調査します。
profilerを使ったリアルタイム観測
Node.jsは--inspect
オプションで起動することで、Chromeと接続してリアルタイムにパフォーマンスプロファイルを取得できます。
起動方法
node --inspect index.js
Chromeでchrome://inspect
にアクセスすると、接続されたプロセスを選択できます。
DevToolsのMemory
タブで「Record Allocation Timeline」や「Heap Snapshot」を活用することで、どのタイミングでメモリ使用量が増加しているのかを特定可能です。
その他の便利ツール
- clinic.js(
npm i -g clinic
):性能やメモリ使用量を統合的に可視化 - v8-profiler-next:カスタムプロファイルを取得して分析
まとめ|Node.jsでもメモリ管理は油断禁物
Node.jsは非同期で効率的に動作する一方で、JavaScriptならではのスコープやクロージャの罠により、気づかぬうちにメモリを食いつぶすケースがあります。
本番環境では、heapdumpや--inspect
を活用した定点観測と可視化が不可欠です。定期的にスナップショットを取得して比較することで、潜在的なリークの兆候を早期に発見できるようになります。
パフォーマンス維持と安定稼働のためにも、メモリリークへの意識を高く持っておきましょう。