Menghurai, Memaparkan, dan Menstrim JSON Besar
Inti pati: Penghuraian selalunya bukan kesesakan; pemaparan dan penyalinan ialah. Optimumkan sepanjang empat lapisan "hurai → telusuri → paparkan → hantar" supaya usaha anda tidak terbuang.

Kesimpulan utama: ukur penghuraian, penelusuran, pemaparan, dan penghantaran secara berasingan dahulu, kemudian bekerja dalam urutan ini — potong salinan, kembang mengikut permintaan dengan tatal maya, pindahkan kerja ke Worker, beralih ke penstriman, dan hanya selepas itu tukar format. Penstriman berbaloi hanya apabila fail tidak muat dalam memori atau apabila anda memerlukan subset kecil medannya.
Kesimpulan "JSON.parse perlahan" salah lapan kali daripada sepuluh. Apa yang sebenarnya perlahan ialah apa yang anda lakukan selepas penghuraian.
1. Ukur Dahulu, Kemudian Optimumkan
Untuk fail JSON 10MB yang sama, masa yang dihabiskan dalam peringkat berbeza berbeza mengikut urutan magnitud:
| Peringkat | Tempoh biasa | Kos utama |
|---|---|---|
JSON.parse (natif) |
Puluhan hingga sedikit melebihi seratus milisaat | Satu imbasan linear serta pembinaan objek |
Salinan mendalam (structuredClone / rekursif) |
Mungkin 1–3 kali penghuraian | Membina semula keseluruhan graf objek |
| Perbandingan mendalam / diff | Mungkin berganda lagi | Penelusuran rekursif serta perbandingan string |
| Pemaparan ke dalam pokok DOM | Membeku sebaik terdapat puluhan ribu nod | Susun atur dan reflow |
| Penggabungan string kemudian pensirian | Berkadar dengan saiz output | Menggandakan memori |
Jadi ukur sebelum bertindak: dalam pelayar, balut setiap peringkat dengan performance.now(), atau gunakan console.time;
dalam Node, gunakan process.memoryUsage() untuk memerhati pertumbuhan heap. Tiada garis dasar, tiada pengoptimuman.
2. Lapisan Penghuraian: Tiga Strategi
2.1 Hurai penuh (pilihan lalai)
const data = JSON.parse(text);
Pelaksanaan natif melakukan pengimbasan dan pembinaan objek dalam C++, jadi pustaka JS tulen hampir tidak dapat mengalahkannya. Gunakannya selagi tiga syarat berikut dipenuhi:
- Teks muat sepenuhnya dalam memori (biasanya < 100MB);
- Kebanyakan medan benar-benar diperlukan selepas penghuraian;
- Tahap ketidakpercayaan data dapat dikendalikan.
2.2 Penghuraian inkremental penstriman
Dengan penghurai gaya SAX seperti stream-json, anda boleh mengimbas keluar medan sasaran tanpa membina pokok objek 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) {
// Process one element at a time; memory is independent of element count
handleItem(value);
}
Kos:
- Kerumitan kod meningkat dengan ketara dan pengendalian kesalahan menjadi lebih kucar-kacir;
- Hanya akses berjujukan yang mungkin, tanpa akses rawak;
- Ia tidak mesra terhadap keperluan yang memerlukan pengagregatan global.
Peraturan keputusan: penstriman hanya berbaloi apabila fail terlalu besar untuk muat, atau apabila anda hanya memerlukan subset medan yang sangat kecil.
2.3 Tukar format
Jika data ialah "sekumpulan rekod" dan bukannya "satu dokumen tunggal", anda sepatutnya menggunakan JSON Lines dari awal. Format berbatas baris menukar "penstriman" menjadi "membaca baris demi baris", menurunkan kerumitan pelaksanaan satu urutan magnitud.
3. Lapisan Penelusuran: Satu Salinan Kurang Memenangi Separuh Pertempuran
// Anti-pattern: deep copying first "for safety"
const safeCopy = structuredClone(bigData); // Memory doubles immediately
// Good pattern: pass references directly in read-only scenarios
function renderSummary(data: OrderDocument): Summary {
return { total: data.items.length, amount: data.total };
}
Salinan tersembunyi biasa:
JSON.parse(JSON.stringify(x))digunakan sebagai "salinan mendalam" — ini jenis paling mahal;{ ...data }hanya menyalin satu lapisan, tetapi untuk objek besar ia masih menyalin tatasusunan rujukan semua kunci tahap atas;Array.prototype.mapmencipta tatasusunan baharu, yang juga mahal apabila terdapat banyak elemen;- Proksi kereaktifan rangka kerja (
reactiveVue) memproksi objek besar secara rekursif, dan overhed boleh lebih tinggi daripada penghuraian itu sendiri.
Nasihat praktikal: kekalkan "data mentah" sebagai objek tidak berubah biasa dan jangan balutnya dalam kereaktifan; kira dan cache data terbitan yang perlu anda paparkan secara berasingan.
4. Lapisan Pemaparan: Jangan Masukkan Seluruh Pokok ke dalam DOM
Fail JSON 5MB mempunyai kira-kira 30,000 hingga 100,000 nod; memaparkan semuanya sebagai elemen DOM akan membuat pelayar berhenti bertindak balas.
Tiga pendekatan yang boleh dilaksanakan:
4.1 Kembang mengikut permintaan
Paparkan hanya anak langsung nod akar, dan kira tahap seterusnya hanya apabila nod dikembangkan. Ini pendekatan standard untuk paparan pokok:
type TreeNode = { key: string; value: unknown; expanded: boolean };
/**
* Returns only the currently visible nodes, avoiding building the whole tree at once.
* @param rootNode Root node
* @param expandedPaths Set of paths that have been expanded
* @returns List of visible nodes
*/
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
const visibleNodes: TreeNode[] = [];
/**
* Recursively collects the child nodes of expanded branches.
* @param currentNode Current node
* @param currentPath Current path
*/
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 Tatal maya
Sebaik nod yang dikembangkan melebihi beberapa ratus, paparkan hanya beberapa puluh di dalam viewport dan simpan ruang untuk selebihnya dengan ketinggian pemegang tempat. Ia paling berkesan pada paparan gaya senarai (jadual, log).
4.3 Tunjukkan ringkasan sahaja
Mula-mula persembahkan "struktur + jenis + kiraan" (contohnya items: Array(12000)); hanya apabila pengguna membuka satu item anda ambil nilai sebenar.
Trik ini paling berkesan untuk senario "saya hanya mahu mengesahkan medan betul".
5. Main Thread: Pindahkan Kerja ke Worker Apabila Patut
Penghuraian, pemformatan, dan pembezaan apa-apa yang lebih besar daripada 1MB harus masuk ke dalam 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 });
}
};
/**
* Parses JSON inside a Worker so the main thread is not blocked.
* @param text JSON text
* @returns Parse result
*/
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 kos komunikasi Worker: postMessage menggunakan pengklonan berstruktur secara lalai, dan mengklon objek 10MB sekali juga menelan puluhan milisaat.
Jika yang anda perlukan hanyalah "hurai dan serahkan kepada main thread", lakukan kerja berat di dalam Worker (hurai + transform + sahkan) dan hantar balik ringkasan hasil.
6. Lapisan Penghantaran: Pengoptimuman Saiz
| Teknik | Keuntungan biasa | Kos |
|---|---|---|
| gzip / brotli | Output dimampatkan selalunya 10%–25% daripada saiz asal | Perlukan sokongan pelayan |
| Buang inden dan ruang putih | Pengurangan 20%–40% | Tidak boleh dibaca manusia |
| Ringkaskan nama medan | Keuntungan ketara dengan bersarang dalam | Kebolehbacaan lebih rendah; perlukan jadual pemetaan |
| Nombor dan bukannya string enum | Kecil | Perlukan kamus |
| Penomboran halaman / medan mengikut permintaan | Terbesar | Reka semula API |
Untuk halaman yang ditujukan untuk manusia, gabungan yang betul ialah: API menghantar JSON padat dan dimampatkan, dan UI mencantikkannya hanya untuk paparan. Banyak laman alat secara lalai menghantar teks yang dicantikkan ke bahagian hadapan semula, menghantar 30% lebih bait tanpa faedah.
7. Cara Membuat Penanda Aras
/**
* Measures the median duration of a synchronous routine so a single jittery run does not skew the result.
* @param runOnce Function under test
* @param repeatCount Number of repetitions
* @returns Median duration in milliseconds
*/
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 peraturan disiplin:
- Gunakan sampel tetap dan jangan sekali-kali menjana data secara rawak, atau nombor tidak boleh dibandingkan;
- Jalankan sekurang-kurangnya 9 kali dan ambil median; larian pertama termasuk pemanasan JIT dan mesti dibuang;
- Rekod persekitaran masa jalan (versi Node, CPU), atau perbandingan merentas mesin tidak bermakna.
8. Senarai Semak
- [ ] Nombor garis dasar wujud supaya sebelum dan selepas boleh dibandingkan;
- [ ] Menghurai apa-apa yang lebih besar daripada 1MB berjalan dalam Worker;
- [ ] Paparan pokok berkembang mengikut permintaan dan senarai menggunakan tatal maya;
- [ ] Tiada salinan mendalam atau balutan reaktif yang tidak perlu;
- [ ] JSON Lines digunakan apabila data ialah "sekumpulan rekod";
- [ ] API menghantar JSON padat dan pemformatan berlaku hanya untuk paparan.