【入門編】Node.jsで巨大なJSONを扱う技術:JSONStreamによるメモリ効率を意識したデータ処理 – 実行環境・ランタイム・コンパイラ生産性向上バイブル

巨大JSONに潜む「メモリの罠」と、Node.jsストリーム処理の真実

こんにちは。日々の開発で「なぜか本番環境でだけOOM(Out of Memory)エラーが起きる」と頭を抱えた経験はありませんか?

多くの場合、犯人は「巨大なJSONファイル」の読み込み処理です。Node.jsで `fs.readFileSync` や `JSON.parse` を使って数GBのファイルを一度にメモリへ展開すれば、V8エンジンのヒープメモリ制限にあっという間に到達します。

今日は、Node.jsのストリーム(Stream)という「バケツリレー」の概念を使って、メモリを一切枯渇させずに数ギガバイトのJSONを捌く技術を伝授します。これをマスターすれば、あなたのシステムは「データ量が増えても壊れない」堅牢なアーキテクチャへと進化します。

—

1. なぜ「JSONStream」なのか?:ストリーム処理の哲学

通常、`JSON.parse()` は文字列全体をメモリにロードする必要があります。しかし、`JSONStream` ライブラリは違います。ファイルの中身を小さな「チャンク(断片)」として読み込み、JSONの構造を解析しながら、データが一個完成するたびにイベントを発火させます。

イメージとしては、「巨大な倉庫から荷物を一気に運び出す」のではなく、「ベルトコンベアに乗って流れてくる荷物を、一つずつその場で検品して箱詰めする」という感覚です。

セットアップ

まずはプロジェクトを作成し、必要なツールを導入しましょう。

プロジェクトの初期化
mkdir json-stream-lab && cd json-stream-lab
npm init -y

JSONStreamのインストール(必須)
fsモジュールはNode.js標準なので追加インストール不要です
npm install JSONStream

—

2. HelloWorld:ストリームによる「メモリに優しい」解析

まずは、巨大なJSON配列(`items.json`)を想定して、中身を一つずつ取り出す最も効率的な書き方を見てみましょう。

items.json (サンプルデータ)

[
{“id”: 1, “name”: “Apple”},
{“id”: 2, “name”: “Banana”},
{“id”: 3, “name”: “Cherry”}
// …実際にはこれが数百万行続く
]

index.js (ストリーム処理の実装)

const fs = require(‘fs’);
const JSONStream = require(‘JSONStream’);

// 読み込みストリームを作成
const stream = fs.createReadStream(‘items.json’, { encoding: ‘utf8’ });

// JSONStreamを使って、配列の各要素(”)をパース対象として指定
const parser = JSONStream.parse(”);

// ストリームをパイプラインで繋ぐ
stream.pipe(parser)
.on(‘data’, (data) => {
// ここで各データが1つずつ処理される。
// メモリには常に現在の1要素分しか存在しないため、OOMは発生しない。
console.log(‘処理したアイテム:’, data.name);
})
.on(‘end’, () => {
console.log(‘— すべての処理が完了しました —‘);
})
.on(‘error’, (err) => {
console.error(‘パースエラー:’, err);
});

このコードの肝は `.pipe()` です。これは「読み込み元(ReadStream)」から「処理者(TransformStream)」へ、データが溢れないように自動的に流量を制御(バックプレッシャー)しながら橋渡しをする魔法のメソッドです。

—

3. 実務で直面する「バッチ処理」の最適化

実務では、「パースしたデータをそのままDBへ投入したい」というケースがほとんどです。ここで注意すべきは「DBへの書き込み速度は、ファイル読み込み速度より圧倒的に遅い」という事実です。

ファイルからデータを高速に読み込みすぎてDBがパンクしないよう、バッチ処理(まとめて挿入)を組み合わせるのがアーキテクトの腕の見せ所です。

const fs = require(‘fs’);
const JSONStream = require(‘JSONStream’);

let batch = [];
const BATCH_SIZE = 100; // 一度に処理する件数

fs.createReadStream(‘large_data.json’)
.pipe(JSONStream.parse(”))
.on(‘data’, async (item) => {
batch.push(item);

// バッチサイズに達したら書き込み
if (batch.length >= BATCH_SIZE) {
// 処理を一時停止(バックプレッシャーの制御)
parser.pause();

await db.insertMany(batch); // ここでDBに一括登録
batch = []; // バッチをクリア

// 再開
parser.resume();
}
});

なぜこのアプローチが最強なのか?

1. 定数メモリ消費: どんなに巨大なファイルでも、メモリ消費量は「バッチサイズ分」で一定です。
2. バックプレッシャー: `parser.pause()` を活用することで、DBの書き込みが完了するまでファイルの読み込みを「待機」させます。これにより、サーバーがデータで溺れることを防ぎます。

—

最後に:エンジニアとしての視点

ストリーム処理を使いこなせるようになると、コードから「ファイルサイズ」という変数が消えます。どんなに大きなデータが来ても、あなたのプログラムは淡々と、しかし確実に仕事をこなす「プロフェッショナルな挙動」を示すようになります。

まずは、手元の巨大なJSONファイルを読み込むところから始めてみてください。「あれ、こんなに速く、かつメモリを食わずに終わるのか!」という感動が、あなたの開発者人生のスタンダードになるはずです。

何か詰まったら、いつでも聞いてくださいね。現場の壁を突破するための知見は、まだまだたくさんありますよ。

タイトルとURLをコピーしました