JSON Toolkit

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.

Pertengahan~13 min bacaanDiterbitkan 2026-10-30Dikemas kini 2026-10-30
Kulit untuk soalan lazim prestasi dan keselamatan

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:

  1. Kurangkan jumlah data: buat API berhalaman, kembalikan hanya medan yang diperlukan, dan hidupkan gzip/brotli;
  2. Elakkan pengklonan dalam: lihat bahagian seterusnya; ini kos tersembunyi yang paling biasa;
  3. Pemaparan malas: dalam paparan pokok paparkan hanya tahap yang dikembangkan dan bukannya menghasilkan puluhan ribu nod DOM serentak;
  4. 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:

  1. Penatalan maya: paparkan hanya beberapa dozen baris di dalam viewport dan guna semula nod DOM semasa menatal;
  2. Runtuhkan secara lalai: kembangkan hanya dua tahap pertama dan paparkan tahap lebih dalam atas permintaan;
  3. 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:

  1. Tapis nama kunci: langkau __proto__, constructor, dan prototype semasa menggabungkan;
  2. Gunakan Object.create(null) sebagai bekas supaya tiada rantaian prototaip untuk dicemari;
  3. Gunakan Map untuk 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:

  1. Hadkan saiz: tolak apa-apa melebihi ambang serta-merta, tanpa membacanya ke dalam memori dahulu;
  2. Hadkan kedalaman: JSON yang bersarang beratus-ratus tahap dalam boleh melimpahi tindanan penghurai rekursif;
  3. Hadkan kiraan kunci: menghalang serangan perlanggaran cincang dan pembesaran memori;
  4. Tapis kunci berbahaya: __proto__, constructor, prototype;
  5. Sahkan struktur: jalankannya melalui Schema untuk mengesahkan jenis dan medan wajib;
  6. 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/json yang betul menghalang pelaksanaan skrip;
  • X-Content-Type-Options: nosniff menghalang 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:

  1. 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;
  2. Suntikan log: nilai JSON yang mengandungi baris baharu dan awalan log palsu memecahkan satu rekod kepada beberapa dan mencemarkan jejak audit;
  3. 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