Soalan Lazim Prestasi dan Keselamatan: Data Perlahan, Tersekat dan Tidak Dipercayai
Inti pati: Mengapa fail JSON berukuran puluhan MB tidak akan dibuka, betapa mahalnya pengklonan dalam, bagaimana menghalang pencemaran __proto__, dan di mana XSS masuk — sepuluh kesimpulan bantuan awal disusun mengikut simptom.

Pokoknya: diagnosis mengikut simptom dahulu — penghuraian, pemaparan atau memori — kemudian ikuti urutan pembetulan: kurangkan jumlah data, hentikan pengklonan dalam, pindahkan penghuraian ke dalam Worker dengan penatalan maya, dan strimkan apa-apa melebihi 50 MB. Untuk input tidak dipercayai, hadkan saiz dan kedalaman bersarang sebelum menapis __proto__, constructor dan prototype.
"JSON perlahan" tidak tepat sembilan kali daripada sepuluh; apa yang sebenarnya perlahan biasanya pemaparan, pengklonan dalam, atau urutan operasi yang salah.
1. Mengapa ia perlahan
Menghuraikan fail JSON yang besar adalah perlahan. Bagaimana saya mengoptimumkannya?
Kenal pasti halangan dahulu, bincang pengoptimuman kemudian. Peringkat berbeza mengikut beberapa susunan magnitud:
| Peringkat | Anggaran masa untuk 10 MB data | Mengapa ia halangan |
|---|---|---|
| Pemindahan rangkaian | Bergantung pada lebar jalur | JSON tanpa mampat biasanya memampat kepada kira-kira 1/5 |
JSON.parse |
20–80 ms | Pelaksanaan asli, sudah pantas |
| Perjalanan rekursif / klon dalam | 200–2000 ms | Pemprosesan nod demi nod JS tulen |
| Pemaparan DOM (paparan pokok) | Saat, mudah membeku | Kiraan nod meletup |
Jadi urutan pengoptimuman sepatutnya:
- Kurangkan jumlah data: buat API berhalaman, kembalikan hanya medan yang diperlukan, dan hidupkan gzip/brotli;
- Elakkan pengklonan dalam: lihat bahagian seterusnya; ini kos tersembunyi yang paling biasa;
- Pemaparan malas: dalam paparan pokok paparkan hanya tahap yang dikembangkan dan bukannya menghasilkan puluhan ribu nod DOM serentak;
- Penstriman: jangan huraikan semuanya serentak apabila data melebihi 10 MB.
Jika halangan benar-benar JSON.parse, itu bermakna data sudah puluhan MB atau lebih, dan langkah yang betul ialah penghurai penstriman:
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
// Kendalikan elemen tatasusunan satu demi satu; penggunaan memori bebas daripada jumlah saiz
const pipeline = fs.createReadStream('big.json')
.pipe(parser())
.pipe(streamArray());
pipeline.on('data', ({ value }) => {
processItem(value);
});
JSON puluhan MB merosakkan pelayar sebaik sahaja dibuka. Apa sekarang?
Sahkan dahulu sama ada ia "rosak" atau "membeku". Kedua-duanya memerlukan pengendalian yang benar-benar berbeza:
- Membeku: tab menunjukkan "Halaman tidak bertindak balas" dan mungkin pulih selepas puluhan saat. Puncanya ialah tugas panjang yang memenuhi benang utama;
- Rosak: tab bertukar menjadi "Aw, Snap", biasanya kehabisan memori (OOM).
Sama ada cara, jangan memprosesnya pada benang utama. Pindahkan penghuraian ke dalam Web Worker:
// worker.js
self.onmessage = (event) => {
const parsed = JSON.parse(event.data);
self.postMessage(parsed);
};
Worker menyelesaikan "UI tidak membeku", bukan had memori. Tab pelayar biasanya mempunyai 1–4 GB tersedia; JSON 200 MB yang dihuraikan kepada objek boleh menduduki 1 GB atau lebih (setiap pasangan kunci-nilai membawa overhed objek), dan dengan pemaparan di atasnya ia mudah mencapai had.
Penyelesaian sebenar ialah jangan sekali-kali memuatkan semuanya:
| Saiz data | Pendekatan |
|---|---|
| < 5 MB | Huraikan dengan JSON.parse terus, awasi pemaparan |
| 5–50 MB | Penghuraian Worker + penatalan maya |
| > 50 MB | Penghuraian penstriman, simpan hanya medan yang diperlukan |
| > 500 MB | Tukar format (Parquet / SQLite / JSON Lines berpecah) |
Adakah JSON.parse(JSON.stringify(obj)) klon dalam?
Ia klon dalam, tetapi ia "mengubah data secara senyap" di beberapa tempat, jadi ia tidak sesuai sebagai alat pengklonan umum.
const source = {
date: new Date(),
fn: () => {},
undef: undefined,
nan: NaN,
map: new Map(),
nested: { a: 1 },
};
JSON.parse(JSON.stringify(source));
// {
// date: "2026-10-30T03:00:00.000Z", // Date menjadi rentetan
// nan: null, // NaN menjadi null
// nested: { a: 1 } // hanya tahap ini betul
// }
// fn dan undef hilang, map menjadi {}
Senarai masalah:
| Jenis | Hasil |
|---|---|
Date |
Menjadi rentetan ISO |
NaN / Infinity |
Menjadi null |
undefined / fungsi / Symbol |
Hilang dalam objek, menjadi null dalam tatasusunan |
Map / Set |
Menjadi {} |
BigInt |
Melontar TypeError |
| Rujukan bulat | Melontar TypeError |
Pelayar moden mempunyai structuredClone, yang menyokong Date, Map, Set, dan rujukan bulat tetapi bukan fungsi:
const copy = structuredClone(source);
Jika ia hanya penggabungan konfigurasi cetek, operator spread ialah pilihan paling murah: { ...config, timeout: 3000 }.
Mengapa mengembangkan paparan pokok begitu tersekat?
Kerana setiap nod ialah elemen DOM, dan sebaik kiraan nod mencapai puluhan ribu kos susun atur pelayar meletup.
JSON 10 MB boleh mengandungi 50k–100k nod daun. Walaupun anda memaparkan hanya kawasan yang kelihatan, membina DOM sekali mengambil masa ratusan milisaat hingga beberapa saat, dan setiap tatalan berikutnya mencetuskan aliran semula.
Pendekatan yang betul:
- Penatalan maya: paparkan hanya beberapa dozen baris di dalam viewport dan guna semula nod DOM semasa menatal;
- Runtuhkan secara lalai: kembangkan hanya dua tahap pertama dan paparkan tahap lebih dalam atas permintaan;
- Hadkan skala pemaparan: melebihi 10k nod, tunjukkan "Data terlalu besar, gunakan carian untuk menemuinya" dan bukannya memaksa pemaparan.
Ini juga sebabnya editor di laman ini merosot dengan anggun untuk fail besar — bukan kerana ia secara teknikal tidak boleh memaparkannya, tetapi kerana memaparkannya akan tidak boleh digunakan.
2. Mengapa ia diserang
Bagaimana suntikan __proto__ berfungsi?
Pencemaran prototaip. Penyerang membina JSON dengan kunci __proto__; jika kod anda menggabungkannya ke dalam objek tanpa menapis,
ia menimpa Object.prototype dan mempengaruhi setiap objek dalam masa jalan.
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Kod berbahaya: gabungan rekursif tanpa menyemak nama kunci
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, payload);
({}).isAdmin; // true — setiap objek telah tercemar
Fakta utama: JSON.parse sendiri tidak mencemarkan prototaip; {"__proto__": ...} dihuraikan menjadi sifat milik biasa.
Yang berbahaya ialah penggabungan, penugasan, Object.assign, lodash.merge, dan operasi serupa selepas penghuraian.
Tiga pertahanan:
- Tapis nama kunci: langkau
__proto__,constructor, danprototypesemasa menggabungkan; - Gunakan
Object.create(null)sebagai bekas supaya tiada rantaian prototaip untuk dicemari; - Gunakan
Mapuntuk menyimpan pasangan kunci-nilai yang tidak dipercayai.
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
const value = source[key];
if (value && typeof value === 'object' && !Array.isArray(value)) {
target[key] = safeMerge(target[key] ?? {}, value);
} else {
target[key] = value;
}
}
return target;
}
Perhatikan juga bahawa for...in merayau sifat yang diwarisi; Object.keys() lebih selamat.
Apa yang perlu saya awasi semasa menghuraikan JSON yang tidak dipercayai?
Lakukannya dalam urutan ini — kos rendah, pulangan tinggi:
- Hadkan saiz: tolak apa-apa melebihi ambang serta-merta, tanpa membacanya ke dalam memori dahulu;
- Hadkan kedalaman: JSON yang bersarang beratus-ratus tahap dalam boleh melimpahi tindanan penghurai rekursif;
- Hadkan kiraan kunci: menghalang serangan perlanggaran cincang dan pembesaran memori;
- Tapis kunci berbahaya:
__proto__,constructor,prototype; - Sahkan struktur: jalankannya melalui Schema untuk mengesahkan jenis dan medan wajib;
- Jangan percaya
Content-Type: jenis yang diisytiharkan dan kandungan sebenar mungkin tidak sepadan.
Semakan kedalaman adalah murah; anda boleh menggunakan penapis regex kasar sebelum penghuraian, atau merentas selepas penghuraian:
function measureDepth(value, depth = 0) {
if (value === null || typeof value !== 'object') {
return depth;
}
let maxDepth = depth;
for (const key of Object.keys(value)) {
maxDepth = Math.max(maxDepth, measureDepth(value[key], depth + 1));
}
return maxDepth;
}
Tetapi kod ini sendiri rekursif dan mungkin masih melimpahi tindanan pada data yang sangat dalam secara berniat jahat. Pendekatan selamat ialah menguatkuasakan had semasa penghuraian,
menggunakan penghurai yang menyokong pilihan kedalaman (sesetengah pustaka menyediakan parameter maxDepth).
Bagaimana XSS melalui JSON berlaku?
JSON sendiri bukan HTML dan tidak dilaksanakan. XSS berlaku pada titik di mana "nilai JSON digabungkan ke dalam HTML".
Tiga laluan biasa:
| Laluan | Contoh | Pertahanan |
|---|---|---|
| Gabungan templat sebelah pelayan | res.send('<div>' + data.name + '</div>') |
Escape HTML pada output |
innerHTML frontend |
el.innerHTML = data.html |
Gunakan textContent atau sanitasi |
| Pembenaman skrip sebaris | <script>var d = ${JSON.stringify(data)}</script> |
Escape <, >, & |
Yang ketiga paling halus. JSON.stringify tidak meng-escape <, jadi data yang mengandungi </script><script>alert(1)</script>
boleh menutup teg skrip lebih awal:
const data = { name: '</script><script>alert(1)</script>' };
// Output: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>
Pembetulan ialah menukar aksara berbahaya kepada escape Unicode:
function safeJsonForHtml(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
}
Patutkah saya mengehadkan kedalaman bersarang?
Ya. Ini salah satu pertahanan dengan nisbah kos-faedah terbaik.
Akibat tidak mengehadkannya: fungsi rekursif melimpahi tindanan, JSON.parse rosak pada sesetengah pelaksanaan apabila kedalaman terlalu besar,
dan pensirian meletupkan tindanan dengan cara yang sama. [[[[...]]]] 1 KB sudah cukup untuk menjatuhkan penghurai tanpa perlindungan.
Ambang yang munasabah bergantung pada perniagaan:
| Jenis data | Had yang dicadangkan |
|---|---|
| Objek perniagaan biasa | 10–20 tahap |
| Struktur pokok (carta organisasi, direktori) | 50–100 tahap |
| Gerbang API umum | 20 tahap |
Apakah itu penculikan JSON?
Kerentanan sejarah: pelayar awal membenarkan <script src> merentas asal memuatkan dan melaksanakan JSON, dan penyerang mencuri data dengan mentakrifkan semula
konstruktor Array atau Object.prototype.__defineSetter__.
Perlindungan moden agak lengkap:
- Pelayar tidak lagi membenarkan penculikan literal tatasusunan dengan
__defineSetter__; Content-Type: application/jsonyang betul menghalang pelaksanaan skrip;X-Content-Type-Options: nosniffmenghalang sniffing MIME.
Sistem lama masih membawa risiko sisa. Nasihat praktikal: gunakan objek pada tahap atas, bukan tatasusunan kosong.
Pembalut seperti {"data": [...]} memastikan respons bukan pernyataan JavaScript yang sah ({ diperlakukan sebagai blok kod),
menyekat penculikan pada peringkat sintaks.
Apakah risiko mencatat log JSON?
Tiga:
- Maklumat sensitif ditulis ke cakera: nombor telefon, nombor pengenalan, token, dan kata laluan teks biasa masuk ke sistem log, yang biasanya mempunyai kebenaran lebih longgar dan penyimpanan lebih lama;
- Suntikan log: nilai JSON yang mengandungi baris baharu dan awalan log palsu memecahkan satu rekod kepada beberapa dan mencemarkan jejak audit;
- Letupan saiz: menulis respons 10 MB ke dalam log memenuhi sistem log dengan cepat.
Pendekatan:
// Sembunyikan dan potong sebelum mencatat log
function safeLog(data) {
const cloned = structuredClone(data);
for (const key of ['password', 'token', 'idCard', 'phone']) {
if (key in cloned) cloned[key] = '***';
}
const text = JSON.stringify(cloned);
return text.length > 2000 ? text.slice(0, 2000) + '...(truncated)' : text;
}
Sistem log berstruktur (seperti pino) meng-escape baris baharu sendiri, tetapi penyembunyian dan pemotongan masih tugas anda.
3. Rujukan pantas
| Simptom | Semak dahulu | Pembetulan |
|---|---|---|
| Penghuraian perlahan | Jumlah data, sama ada pengklonan dalam terlibat | Pembahagian/penstriman, buang JSON.parse(JSON.stringify()) |
| Halaman membeku | Tugas panjang pada benang utama, bilangan nod yang dipaparkan | Worker + penatalan maya |
| Tab rosak | Penggunaan memori | Jangan muatkan semuanya; beralih kepada penstriman |
| Klon dalam kehilangan data | Date, NaN, Map |
Gunakan structuredClone |
| Objek tiba-tiba mempunyai sifat tambahan | Pencemaran prototaip | Tapis __proto__, gunakan Map |
| Skrip muncul | Rentetan digabungkan ke dalam HTML | Escape <, gunakan textContent |
| Tindanan melimpah | Kedalaman bersarang | Hadkan kepada 10–20 tahap |
| Log penuh | Seluruh respons dicatat | Potong + sembunyikan |