FAQ Performa dan Keamanan: Lambat, Tersendat, dan Data Tidak Tepercaya
Intinya: Mengapa berkas JSON berukuran puluhan MB tidak mau terbuka, betapa mahalnya deep clone, bagaimana mencegah polusi __proto__, dan di mana XSS masuk — sepuluh kesimpulan pertolongan pertama yang disusun menurut gejala.

Intinya: diagnosis berdasarkan gejala dulu — parsing, rendering, atau memori — lalu jalankan urutan perbaikan: kurangi volume data, hentikan deep cloning, pindahkan parsing ke Worker dengan virtual scrolling, dan alirkan (streaming) apa pun di atas 50 MB. Untuk input tidak tepercaya, batasi ukuran dan kedalaman bersarang sebelum menyaring __proto__, constructor, dan prototype.
"JSON itu lambat" sembilan dari sepuluh kali tidak akurat; yang sebenarnya lambat biasanya rendering, deep cloning, atau urutan operasi yang salah.
1. Mengapa lambat
Parsing berkas JSON besar lambat. Bagaimana mengoptimalkannya?
Temukan bottleneck dulu, bicara optimasi kemudian. Tahap-tahapnya berbeda beberapa orde besaran:
| Tahap | Perkiraan waktu untuk data 10 MB | Mengapa menjadi bottleneck |
|---|---|---|
| Transfer jaringan | Bergantung pada bandwidth | JSON tanpa kompresi biasanya terkompresi menjadi sekitar 1/5 |
JSON.parse |
20–80 ms | Implementasi native, sudah cepat |
| Traversal rekursif / deep clone | 200–2000 ms | Pemrosesan node demi node dalam JS murni |
| Rendering DOM (tampilan pohon) | Beberapa detik, mudah membeku | Jumlah node meledak |
Jadi urutan optimasinya seharusnya:
- Kurangi volume data: buat API melakukan paginasi, kembalikan hanya field yang dibutuhkan, dan aktifkan gzip/brotli;
- Hindari deep cloning: lihat bagian berikutnya; ini adalah biaya tersembunyi yang paling umum;
- Rendering malas (lazy): di tampilan pohon render hanya level yang diperluas alih-alih menghasilkan puluhan ribu node DOM sekaligus;
- Streaming: jangan parse semuanya sekaligus ketika data melebihi 10 MB.
Jika bottleneck-nya memang JSON.parse, itu berarti data sudah berukuran puluhan MB atau lebih, dan langkah yang tepat adalah parser streaming:
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
// Tangani elemen array satu per satu; penggunaan memori tidak bergantung pada ukuran total
const pipeline = fs.createReadStream('big.json')
.pipe(parser())
.pipe(streamArray());
pipeline.on('data', ({ value }) => {
processItem(value);
});
JSON berukuran puluhan MB membuat browser crash begitu dibuka. Apa yang harus dilakukan?
Pastikan dulu apakah ia "crash" atau "membeku". Keduanya membutuhkan penanganan yang sangat berbeda:
- Membeku: tab menampilkan "Page unresponsive" dan mungkin pulih setelah puluhan detik. Penyebabnya adalah long task yang menjenuhkan main thread;
- Crash: tab berubah menjadi "Aw, Snap", biasanya karena kehabisan memori (OOM).
Apa pun itu, jangan memprosesnya di main thread. Pindahkan parsing ke Web Worker:
// worker.js
self.onmessage = (event) => {
const parsed = JSON.parse(event.data);
self.postMessage(parsed);
};
Worker menyelesaikan masalah "UI tidak membeku", bukan batas memori. Tab browser biasanya memiliki 1–4 GB yang tersedia; JSON 200 MB yang di-parse menjadi objek bisa menempati 1 GB atau lebih (setiap pasangan key-value membawa overhead objek), dan dengan rendering di atasnya mudah mencapai batas.
Solusi sebenarnya adalah jangan pernah memuat semuanya:
| Ukuran data | Pendekatan |
|---|---|
| < 5 MB | Parse langsung dengan JSON.parse, perhatikan rendering |
| 5–50 MB | Parsing di Worker + virtual scrolling |
| > 50 MB | Parsing streaming, pertahankan hanya field yang dibutuhkan |
| > 500 MB | Ubah format (Parquet / SQLite / JSON Lines terpecah) |
Apakah JSON.parse(JSON.stringify(obj)) sebuah deep clone?
Itu deep clone, tetapi ia "mengubah data secara diam-diam" di beberapa tempat, jadi tidak cocok sebagai alat cloning 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 string
// nan: null, // NaN menjadi null
// nested: { a: 1 } // hanya level ini yang benar
// }
// fn dan undef menghilang, map menjadi {}
Daftar masalahnya:
| Tipe | Hasil |
|---|---|
Date |
Menjadi string ISO |
NaN / Infinity |
Menjadi null |
undefined / fungsi / Symbol |
Menghilang di dalam objek, menjadi null di dalam array |
Map / Set |
Menjadi {} |
BigInt |
Melempar TypeError |
| Referensi melingkar | Melempar TypeError |
Browser modern memiliki structuredClone, yang mendukung Date, Map, Set, dan referensi melingkar tetapi tidak fungsi:
const copy = structuredClone(source);
Jika yang dibutuhkan hanya penggabungan konfigurasi dangkal, operator spread adalah opsi termurah: { ...config, timeout: 3000 }.
Mengapa memperluas tampilan pohon begitu tersendat?
Karena setiap node adalah elemen DOM, dan begitu jumlah node mencapai puluhan ribu, biaya layout browser meledak.
JSON 10 MB bisa memuat 50k–100k node daun. Bahkan jika Anda hanya merender area yang terlihat, membangun DOM sekali butuh ratusan milidetik sampai beberapa detik, dan setiap scroll berikutnya memicu reflow.
Pendekatan yang benar:
- Virtual scrolling: render hanya beberapa lusin baris di dalam viewport dan gunakan kembali node DOM saat menggulir;
- Lipat secara default: perluas hanya dua level pertama dan render level lebih dalam sesuai permintaan;
- Batasi skala render: di atas 10k node, tampilkan "Data terlalu besar, gunakan pencarian untuk menemukannya" alih-alih memaksa render.
Inilah juga alasan editor di situs ini menurun secara mulus untuk berkas besar — bukan karena secara teknis tidak dapat merendernya, tetapi karena merendernya akan tak terpakai.
2. Mengapa menjadi sasaran serangan
Bagaimana injeksi __proto__ bekerja?
Prototype pollution. Penyerang menyusun JSON dengan key __proto__; jika kode Anda menggabungkannya ke sebuah objek tanpa penyaringan,
ia menimpa Object.prototype dan memengaruhi setiap objek di runtime.
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Kode berbahaya: merge rekursif tanpa memeriksa nama key
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 kuncinya: JSON.parse sendiri tidak mencemari prototipe; {"__proto__": ...} diurai menjadi properti own biasa.
Yang berbahaya adalah merge, assignment, Object.assign, lodash.merge, dan operasi serupa setelah parsing.
Tiga pertahanan:
- Saring nama key: lewati
__proto__,constructor, danprototypesaat melakukan merge; - Gunakan
Object.create(null)sebagai wadah sehingga tidak ada rantai prototipe yang bisa tercemar; - Gunakan
Mapuntuk menyimpan pasangan key-value yang tidak tepercaya.
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 bahwa for...in berjalan melalui properti warisan; Object.keys() lebih aman.
Apa yang perlu saya waspadai saat mem-parsing JSON yang tidak tepercaya?
Lakukan dengan urutan ini — biaya rendah, manfaat tinggi:
- Batasi ukuran: tolak apa pun di atas ambang segera, tanpa membacanya ke memori lebih dulu;
- Batasi kedalaman: JSON yang bersarang ratusan level dapat meluap ke stack parser rekursif;
- Batasi jumlah key: mencegah serangan tabrakan hash dan amplifikasi memori;
- Saring key berbahaya:
__proto__,constructor,prototype; - Validasi struktur: jalankan melalui Schema untuk memastikan tipe dan field wajib;
- Jangan memercayai
Content-Type: tipe yang dideklarasikan dan konten sebenarnya bisa tidak sesuai.
Pemeriksaan kedalaman itu murah; Anda bisa memakai filter regex kasar sebelum parsing, atau melakukan traversal setelah parsing:
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 kode ini sendiri rekursif dan mungkin masih meluap ke stack pada data yang sengaja dibuat sangat dalam. Pendekatan aman adalah menegakkan batas selama parsing,
memakai parser yang mendukung opsi kedalaman (sebagian pustaka menyediakan parameter maxDepth).
Bagaimana XSS melalui JSON terjadi?
JSON sendiri bukan HTML dan tidak dieksekusi. XSS terjadi pada titik di mana "nilai JSON dirangkai menjadi HTML".
Tiga jalur umum:
| Jalur | Contoh | Pertahanan |
|---|---|---|
| Perangkaian template di server | res.send('<div>' + data.name + '</div>') |
Escape HTML saat output |
innerHTML di frontend |
el.innerHTML = data.html |
Gunakan textContent atau sanitasi |
| Penyematan skrip inline | <script>var d = ${JSON.stringify(data)}</script> |
Escape <, >, & |
Jalur ketiga adalah yang paling halus. JSON.stringify tidak meng-escape <, sehingga data yang berisi </script><script>alert(1)</script>
dapat menutup tag skrip lebih awal:
const data = { name: '</script><script>alert(1)</script>' };
// Keluaran: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>
Perbaikannya adalah mengubah karakter berbahaya menjadi 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');
}
Haruskah saya membatasi kedalaman bersarang?
Ya. Ini salah satu pertahanan dengan rasio manfaat-biaya terbaik.
Konsekuensi tidak membatasinya: fungsi rekursif meluap ke stack, JSON.parse crash pada sebagian implementasi ketika kedalaman terlalu besar,
dan serialisasi merusak stack dengan cara yang sama. [[[[...]]]] berukuran 1 KB sudah cukup untuk menjatuhkan parser yang tidak terlindungi.
Ambang yang wajar bergantung pada bisnis:
| Tipe data | Batas yang disarankan |
|---|---|
| Objek bisnis biasa | 10–20 level |
| Struktur pohon (bagan organisasi, direktori) | 50–100 level |
| Gateway API umum | 20 level |
Apa itu JSON hijacking?
Kerentanan historis: browser awal mengizinkan <script src> lintas origin untuk memuat dan mengeksekusi JSON, dan penyerang mencuri data dengan mendefinisikan ulang
konstruktor Array atau Object.prototype.__defineSetter__.
Perlindungan modern cukup lengkap:
- Browser tidak lagi mengizinkan pembajakan literal array dengan
__defineSetter__; Content-Type: application/jsonyang benar mencegah eksekusi skrip;X-Content-Type-Options: nosniffmencegah sniffing MIME.
Sistem lama masih membawa risiko sisa. Saran praktis: gunakan objek di level teratas, bukan array telanjang.
Pembungkus seperti {"data": [...]} menjaga respons agar tidak menjadi pernyataan JavaScript yang valid ({ diperlakukan sebagai blok kode),
memblokir hijacking di tingkat sintaks.
Apa risiko melakukan logging JSON?
Tiga:
- Informasi sensitif tertulis ke disk: nomor telepon, nomor identitas, token, dan kata sandi plaintext masuk ke sistem logging, yang biasanya memiliki izin lebih longgar dan retensi lebih lama;
- Injeksi log: nilai JSON yang berisi baris baru dan prefiks log palsu memecah satu catatan menjadi beberapa dan mencemari jejak audit;
- Ledakan ukuran: menuliskan seluruh respons 10 MB ke log memenuhi sistem logging dengan cepat.
Pendekatan:
// Redaksi dan potong sebelum logging
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 logging terstruktur (seperti pino) meng-escape baris baru sendiri, tetapi redaksi dan pemotongan tetap menjadi tugas Anda.
3. Rujukan cepat
| Gejala | Periksa dulu | Perbaikan |
|---|---|---|
| Parsing lambat | Volume data, apakah ada deep cloning | Chunking/streaming, hapus JSON.parse(JSON.stringify()) |
| Halaman membeku | Long task di main thread, jumlah node yang dirender | Worker + virtual scrolling |
| Tab crash | Penggunaan memori | Jangan memuat semuanya; beralih ke streaming |
| Deep clone kehilangan data | Date, NaN, Map |
Gunakan structuredClone |
| Objek tiba-tiba punya properti tambahan | Prototype pollution | Saring __proto__, gunakan Map |
| Muncul popup skrip | String dirangkai menjadi HTML | Escape <, gunakan textContent |
| Stack overflow | Kedalaman bersarang | Batasi ke 10–20 level |
| Log cepat penuh | Seluruh respons di-log | Potong + redaksi |