JSON Toolkit

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.

Menengah±13 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-31
Sampul untuk Parsing, Rendering, dan Streaming JSON Besar

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.map membuat array baru, yang juga mahal ketika elemennya banyak;
  • Proxy reaktivitas framework (reactive Vue) 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:

  1. Gunakan sampel tetap dan jangan pernah menghasilkan data secara acak, atau angkanya tidak bisa dibandingkan;
  2. Jalankan setidaknya 9 kali dan ambil median; putaran pertama mencakup pemanasan JIT dan harus dibuang;
  3. 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.