大きな JSON のパースを速くする
要点:順序を誤った最適化はライブラリの入れ替えを無駄にします。ネットワーク、パース、描画を別々に測り、パース回数を減らし、ペイロードを縮め、ストリーミングします。

要点:まずネットワーク、パース、描画を別々に測りましょう。「遅いパース」の多くは実は遅い描画です。それから順に登りましょう。パース回数を減らし、ペイロードを小さくし、ストリーミングし、Worker を使い、バイナリ形式は最後です。
ライブラリの入れ替えから始めないでください。タイマーを一つ足せば、通常は描画が問題だと分かります。
ステップ 1:段階を測る
const t0 = performance.now();
const response = await fetch(url);
const text = await response.text();
const t1 = performance.now();
const data = JSON.parse(text);
const t2 = performance.now();
render(data);
const t3 = performance.now();
| 支配的な段階 | 本当の問題 | 修正 |
|---|---|---|
| ネットワーク | ペイロードが大きすぎる | フィールド選択、ページネーション、圧縮 |
JSON.parse |
本当に大きな入力 | パース回数削減、ストリーミング、Worker |
| 描画 | 数万ノードを一度に | 仮想リスト、ページング、折りたたみ |
現代のブラウザは 1MB をおよそ 5〜15ms、10MB を 80〜150ms でパースします。10MB に二秒かかるなら、JSON.parse はボトルネックではありません。
ステップ 2:パースを減らす
最も安い利得で、最も見落とされます。
- 同じペイロードを繰り返しパースするのをやめる。 十のコンポーネントがそれぞれ
JSON.parse(localStorage.getItem(...))を呼べば十回のパースです。モジュールレベルで一度だけキャッシュします。 - 深いコピーに
JSON.parse(JSON.stringify(x))を使うのをやめる。 データ量の二倍以上かかります。structuredCloneはそうではありません。 - メインスレッドでパースしてからオブジェクトを Worker に送らない。
postMessageがさらに複製します。生のテキストを送り、Worker にパースさせます。 - フィールドを少なく要求する。 UI が三つのプロパティを必要とするなら、API は三百を送るべきではありません。
ステップ 3:ペイロードを縮める
page/size によるページネーション、深い offset の代わりにカーソル、リストエンドポイントの要約フィールド。これは通常、フロントエンドの最適化すべてを合わせた以上に効きます。ネットワーク、パース、メモリを一度に減らすからです。
ステップ 4:ストリーミングする
データを全体として処理しなければならず、本当に大きいとき:
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
const pipeline = fs.createReadStream('big.json').pipe(parser()).pipe(streamArray());
for await (const { value } of pipeline) {
await handleOne(value);
}
コードは増え、ランダムアクセスもありません。レコード単位の処理にだけ適します。
ステップ 5:メインスレッドから移す
パースは速いがブロックします。Worker に入れ、テキストを渡します。パース済みオブジェクトを渡すと structured clone でコストが倍になります。
ステップ 6:形式を変える、最後に
常に数十 MB の内部通信なら、NDJSON は自然にストリーミングでき、Protobuf や MessagePack はより小さく速いです。どちらも調整を要するプロトコル変更です。
やってはいけないこと
- JSON を
evalする。安全でなく、速くもない - JSON を JSON に埋め込むために stringify する。サイズが倍になる
- 数十万の小さな文字列をループでパースする。バッチにする
FAQ
パースと描画のどちらがボトルネックかを知るには?
リクエスト、テキスト読み取り、JSON.parse、描画を performance.now() で別々に計時し、最大の段階を本当のボトルネックとみなします。現代のブラウザは 1MB をおよそ 5〜15ms、10MB を 80〜150ms でパースするので、10MB のペイロードに二秒かかるのは稀に JSON.parse の問題ではありません。
ライブラリの入れ替えが通常役に立たないのはなぜですか?
JSON.parse はすでにネイティブで速く、「遅いパース」の多くは繰り返しパースか数万ノードの一度の描画から来ます。ライブラリの入れ替えは 10〜30% を稼ぎますが、完全なパースを一つ除去すれば半分を節約できることが多いです。
パースを Web Worker に移すべきですか?
パースがメインスレッドをブロックし、ページが固まるかアニメーションがフレームを落とすときに Worker へ移します。パース済みオブジェクトではなく生のテキストを渡してください。さもないと postMessage の structured clone がコストを倍にします。