Parsing, Rendering, dan Streaming JSON Besar
Intinya: Parsing sering bukan hambatan; rendering dan penyalinanlah yang menjadi hambatan. Optimalkan sepanjang empat lapisan "parse → traverse → render → transport" agar usaha Anda tidak sia-sia.

Intinya: ukur parsing, traversal, rendering, dan transport secara terpisah dulu, lalu bekerja dalam urutan ini — potong salinan, perluas sesuai permintaan dengan virtual scrolling, pindahkan pekerjaan ke Worker, beralih ke streaming, dan baru setelah itu ubah format. Streaming layak hanya ketika file tidak muat di memori atau ketika Anda hanya butuh sebagian kecil fieldnya.
Kesimpulan "JSON.parse lambat" salah delapan dari sepuluh kali. Yang sebenarnya lambat adalah apa yang Anda lakukan setelah parsing.
1. Ukur Dulu, Optimasi Kemudian
Untuk file JSON 10MB yang sama, waktu yang dihabiskan di tahap berbeda berbeda beberapa orde:
| Tahap | Durasi umum | Biaya utama |
|---|---|---|
JSON.parse (native) |
Puluhan hingga sedikit di atas seratus milidetik | Satu pemindaian linear plus konstruksi object |
Deep copy (structuredClone / rekursif) |
Mungkin 1–3 kali waktu parse | Membangun ulang seluruh object graph |
| Deep comparison / diff | Mungkin dua kali lagi | Traversal rekursif plus perbandingan string |
| Rendering ke tree DOM | Membeku begitu ada puluhan ribu node | Layout dan reflow |
| Konkatenasi string lalu serialisasi | Proporsional dengan ukuran output | Menggandakan memori |
Jadi ukur sebelum bertindak: di browser, bungkus setiap tahap dengan performance.now(), atau gunakan console.time;
di Node, gunakan process.memoryUsage() untuk mengawasi pertumbuhan heap. Tanpa baseline, tanpa optimasi.
2. Lapisan Parsing: Tiga Strategi
2.1 Parse penuh (pilihan default)
const data = JSON.parse(text);
Implementasi native melakukan pemindaian dan konstruksi object di C++, jadi pustaka JS murni nyaris tidak bisa mengalahkannya. Gunakan ia selama tiga kondisi berikut terpenuhi:
- Teks sepenuhnya muat di memori (biasanya < 100MB);
- Sebagian besar field benar-benar dibutuhkan setelah parsing;
- Tingkat ketidakpercayaan data masih bisa dikelola.
2.2 Parsing inkremental streaming
Dengan parser bergaya SAX seperti stream-json, Anda bisa memindai field target tanpa membangun pohon object lengkap:
import { createReadStream } from 'node:fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';
const pipeline = createReadStream('huge.json')
.pipe(parser())
.pipe(pick({ filter: 'items' }))
.pipe(streamArray());
for await (const { value } of pipeline) {
// Proses satu elemen pada satu waktu; memori tidak bergantung pada jumlah elemen
handleItem(value);
}
Biayanya:
- Kompleksitas kode naik signifikan dan penanganan error menjadi lebih berantakan;
- Hanya akses sekuensial yang mungkin, tanpa random access;
- Ia tidak ramah untuk kebutuhan yang memerlukan agregasi global.
Aturan keputusan: streaming hanya layak ketika file terlalu besar untuk muat, atau ketika Anda hanya butuh sebagian sangat kecil dari fieldnya.
2.3 Ubah format
Jika data adalah "sekumpulan record" alih-alih "satu dokumen", Anda seharusnya memakai JSON Lines sejak awal. Format berdelimiter baris mengubah "streaming" menjadi "membaca baris per baris", menurunkan kompleksitas implementasi satu orde.
3. Lapisan Traversal: Satu Salinan Lebih Sedikit Memenangkan Setengah Pertempuran
// Anti-pola: deep copy dulu "demi keamanan"
const safeCopy = structuredClone(bigData); // Memori langsung berlipat dua
// Pola baik: oper referensi langsung dalam skenario read-only
function renderSummary(data: OrderDocument): Summary {
return { total: data.items.length, amount: data.total };
}
Salinan tersembunyi yang umum:
JSON.parse(JSON.stringify(x))dipakai sebagai "deep copy" — ini jenis yang paling mahal;{ ...data }hanya menyalin satu level, tetapi untuk object besar ia tetap menyalin array referensi semua key top level;Array.prototype.mapmembuat array baru, yang juga mahal ketika elemennya banyak;- Proxy reaktivitas framework (
reactiveVue) mem-proxy object besar secara rekursif, dan overhead-nya bisa lebih tinggi daripada parsing itu sendiri.
Saran praktis: simpan "data mentah" sebagai plain object immutable dan jangan membungkusnya dengan reaktivitas; hitung dan cache data turunan yang perlu ditampilkan secara terpisah.
4. Lapisan Rendering: Jangan Menjejalkan Seluruh Tree ke DOM
File JSON 5MB punya kira-kira 30.000 sampai 100.000 node; merender semuanya sebagai elemen DOM akan membuat browser berhenti merespons.
Tiga pendekatan yang layak:
4.1 Perluas sesuai permintaan
Render hanya anak langsung dari node root, dan hitung level berikutnya hanya ketika node diperluas. Ini pendekatan standar untuk tree viewer:
type TreeNode = { key: string; value: unknown; expanded: boolean };
/**
* Mengembalikan hanya node yang terlihat saat ini, menghindari pembangunan seluruh tree sekaligus.
* @param rootNode Node root
* @param expandedPaths Himpunan path yang sudah diperluas
* @returns Daftar node yang terlihat
*/
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
const visibleNodes: TreeNode[] = [];
/**
* Mengumpulkan node anak dari cabang yang diperluas secara rekursif.
* @param currentNode Node saat ini
* @param currentPath Path saat ini
*/
const walk = (currentNode: TreeNode, currentPath: string): void => {
visibleNodes.push(currentNode);
if (!expandedPaths.has(currentPath) || typeof currentNode.value !== 'object' || currentNode.value === null) {
return;
}
for (const [childKey, childValue] of Object.entries(currentNode.value)) {
walk({ key: childKey, value: childValue, expanded: false }, `${currentPath}/${childKey}`);
}
};
walk(rootNode, '');
return visibleNodes;
}
4.2 Virtual scrolling
Begitu node yang diperluas melebihi beberapa ratus, render hanya beberapa lusin di dalam viewport dan sisakan ruang untuk sisanya dengan tinggi placeholder. Ini paling efektif pada tampilan bergaya daftar (tabel, log).
4.3 Tampilkan ringkasan saja
Sajikan dulu "struktur + tipe + jumlah" (misalnya items: Array(12000)); hanya ketika pengguna membuka sebuah item Anda mengambil nilai konkretnya.
Trik ini paling cocok untuk skenario "saya hanya ingin memastikan field-nya benar".
5. Main Thread: Pindahkan Pekerjaan ke Worker Saat Memang Perlu
Parsing, format, dan diff untuk apa pun di atas 1MB sebaiknya masuk ke Web Worker:
// json.worker.ts
self.onmessage = (event: MessageEvent<{ text: string }>) => {
try {
const parsed = JSON.parse(event.data.text);
self.postMessage({ ok: true, data: parsed });
} catch (parseError) {
self.postMessage({ ok: false, message: (parseError as Error).message });
}
};
/**
* Mem-parse JSON di dalam Worker agar main thread tidak terblokir.
* @param text Teks JSON
* @returns Hasil parsing
*/
function parseInWorker(text: string): Promise<unknown> {
return new Promise((resolvePromise, rejectPromise) => {
const worker = new Worker(new URL('./json.worker.ts', import.meta.url), { type: 'module' });
worker.onmessage = (event) => {
worker.terminate();
if (event.data.ok) {
resolvePromise(event.data.data);
} else {
rejectPromise(new Error(event.data.message));
}
};
worker.postMessage({ text });
});
}
Perhatikan biaya komunikasi Worker: postMessage memakai structured cloning secara default, dan meng-clone object 10MB sekali pun memakan puluhan milidetik.
Jika yang Anda butuhkan hanya "parse lalu serahkan ke main thread", lakukan pekerjaan berat di dalam Worker (parse + transform + validate) dan kirim kembali ringkasan hasil.
6. Lapisan Transport: Optimasi Ukuran
| Teknik | Keuntungan umum | Biaya |
|---|---|---|
| gzip / brotli | Output terkompresi sering 10%–25% dari ukuran asli | Butuh dukungan server |
| Menghapus indentasi dan whitespace | Pengurangan 20%–40% | Tidak terbaca manusia |
| Menyingkat nama field | Keuntungan terasa pada nesting dalam | Keterbacaan lebih rendah; butuh tabel pemetaan |
| Number alih-alih string enum | Kecil | Butuh kamus |
| Pagination / field sesuai permintaan | Terbesar | Desain ulang API |
Untuk halaman yang ditujukan bagi manusia, kombinasi yang benar adalah: API mengirim JSON compact dan terkompresi, dan UI mempercantiknya hanya untuk tampilan. Banyak situs alat secara default mengirim teks yang sudah dipercantik lagi ke frontend, mengirim 30% byte lebih banyak tanpa alasan.
7. Cara Melakukan Benchmark
/**
* Mengukur durasi median rutin sinkron agar satu putaran yang naik-turun tidak memiringkan hasil.
* @param runOnce Fungsi yang diuji
* @param repeatCount Jumlah pengulangan
* @returns Durasi median dalam milidetik
*/
function measureMedian(runOnce: () => void, repeatCount = 9): number {
const durations: number[] = [];
for (let roundIndex = 0; roundIndex < repeatCount; roundIndex += 1) {
const startedAt = performance.now();
runOnce();
durations.push(performance.now() - startedAt);
}
durations.sort((firstValue, secondValue) => firstValue - secondValue);
return durations[Math.floor(durations.length / 2)];
}
Tiga aturan disiplin:
- Gunakan sampel tetap dan jangan pernah menghasilkan data secara acak, atau angkanya tidak bisa dibandingkan;
- Jalankan setidaknya 9 kali dan ambil median; putaran pertama mencakup pemanasan JIT dan harus dibuang;
- Catat lingkungan runtime (versi Node, CPU), atau perbandingan lintas mesin menjadi tidak bermakna.
8. Checklist
- [ ] Angka baseline ada sehingga sebelum dan sesudah bisa dibandingkan;
- [ ] Parsing untuk apa pun di atas 1MB berjalan di Worker;
- [ ] Tree view diperluas sesuai permintaan dan daftar memakai virtual scrolling;
- [ ] Tidak ada deep copy atau pembungkusan reaktif yang tidak berguna;
- [ ] JSON Lines dipakai ketika data adalah "sekumpulan record";
- [ ] API mengirim JSON compact dan format hanya terjadi untuk tampilan.