FAQ su prestazioni e sicurezza: dati lenti, a scatti e non fidati
In sintesi: Perché un file JSON da decine di MB non si apre, quanto costa un deep clone, come prevenire l'inquinamento di __proto__ e da dove entra l'XSS — dieci conclusioni di pronto soccorso ordinate per sintomo.

In sintesi: diagnostica prima per sintomo — parsing, rendering o memoria — poi segui l'ordine di correzione: riduci il volume dei dati, elimina il deep clone, sposta il parsing in un Worker con scorrimento virtuale e usa lo streaming per qualsiasi cosa sopra i 50 MB. Per input non fidati, limita dimensione e profondità di annidamento prima di filtrare __proto__, constructor e prototype.
"JSON è lento" è impreciso nove volte su dieci; ciò che è davvero lento di solito è il rendering, il deep clone o l'ordine sbagliato delle operazioni.
1. Perché è lento
Il parsing di un file JSON grande è lento. Come lo ottimizzo?
Individua prima il collo di bottiglia, poi parla di ottimizzazione. Le fasi differiscono di diversi ordini di grandezza:
| Fase | Tempo approssimativo per 10 MB di dati | Perché è il collo di bottiglia |
|---|---|---|
| Trasferimento di rete | Dipende dalla larghezza di banda | Il JSON non compresso di solito si comprime a circa 1/5 |
JSON.parse |
20–80 ms | Un'implementazione nativa, già veloce |
| Attraversamento ricorsivo / deep clone | 200–2000 ms | Elaborazione nodo per nodo in puro JS |
| Rendering DOM (vista ad albero) | Secondi, si blocca facilmente | Il numero di nodi esplode |
Quindi l'ordine di ottimizzazione dovrebbe essere:
- Riduci il volume dei dati: fai in modo che l'API pagini, restituisca solo i campi necessari e abiliti gzip/brotli;
- Evita il deep clone: vedi la sezione successiva; è il costo nascosto più comune;
- Rendering pigro: nella vista ad albero renderizza solo i livelli espansi invece di generare decine di migliaia di nodi DOM in una volta;
- Streaming: non analizzare tutto in una volta quando i dati superano i 10 MB.
Se il collo di bottiglia è davvero JSON.parse, significa che i dati sono già decine di MB o più, e la mossa giusta è un parser in streaming:
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
// Handle array elements one by one; memory usage is independent of the total size
const pipeline = fs.createReadStream('big.json')
.pipe(parser())
.pipe(streamArray());
pipeline.on('data', ({ value }) => {
processItem(value);
});
Un JSON da decine di MB manda in crash il browser appena lo apro. E ora?
Prima conferma se "manda in crash" o "si blocca". I due casi richiedono gestioni completamente diverse:
- Blocco: la scheda mostra "Pagina non risponde" e può riprendersi dopo decine di secondi. La causa sono task lunghi che saturano il thread principale;
- Crash: la scheda diventa "Aw, Snap", di solito per esaurimento della memoria (OOM).
In entrambi i casi, non elaborarlo sul thread principale. Sposta il parsing in un Web Worker:
// worker.js
self.onmessage = (event) => {
const parsed = JSON.parse(event.data);
self.postMessage(parsed);
};
Un Worker risolve "l'interfaccia non si blocca", non il limite di memoria. Una scheda del browser ha in genere 1–4 GB disponibili; un JSON da 200 MB analizzato in oggetti può occupare 1 GB o più (ogni coppia chiave-valore porta con sé un overhead dell'oggetto), e con il rendering in più si raggiunge facilmente il limite.
La soluzione reale è non caricarlo mai tutto:
| Dimensione dei dati | Approccio |
|---|---|
| < 5 MB | Analizza direttamente con JSON.parse, tieni d'occhio il rendering |
| 5–50 MB | Parsing in Worker + scorrimento virtuale |
| > 50 MB | Parsing in streaming, conserva solo i campi necessari |
| > 500 MB | Cambia formato (Parquet / SQLite / JSON Lines suddiviso) |
JSON.parse(JSON.stringify(obj)) è un deep clone?
È un deep clone, ma "cambia silenziosamente i dati" in diversi punti, quindi non è adatto come strumento di clonazione generico.
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 becomes a string
// nan: null, // NaN becomes null
// nested: { a: 1 } // only this level is correct
// }
// fn and undef disappear, map becomes {}
L'elenco dei problemi:
| Tipo | Risultato |
|---|---|
Date |
Diventa una stringa ISO |
NaN / Infinity |
Diventa null |
undefined / funzioni / Symbol |
Scompare dentro gli oggetti, diventa null dentro gli array |
Map / Set |
Diventa {} |
BigInt |
Lancia TypeError |
| Riferimenti circolari | Lancia TypeError |
I browser moderni hanno structuredClone, che supporta Date, Map, Set e i riferimenti circolari ma non le funzioni:
const copy = structuredClone(source);
Se si tratta solo di un merge di configurazione superficiale, lo spread operator è l'opzione più economica: { ...config, timeout: 3000 }.
Perché espandere la vista ad albero è così a scatti?
Perché ogni nodo è un elemento DOM, e una volta che il numero di nodi raggiunge decine di migliaia il costo di layout del browser esplode.
Un JSON da 10 MB può contenere 50k–100k nodi foglia. Anche se renderizzi solo l'area visibile, costruire il DOM una volta richiede da centinaia di millisecondi a secondi, e ogni scorrimento successivo innesca un reflow.
L'approccio corretto:
- Scorrimento virtuale: renderizza solo le poche decine di righe dentro il viewport e riutilizza i nodi DOM durante lo scorrimento;
- Compressione per impostazione predefinita: espandi solo i primi due livelli e renderizza i livelli più profondi su richiesta;
- Limita la scala di rendering: sopra i 10k nodi, mostra "Dati troppo grandi, usa la ricerca per localizzarli" invece di forzare un rendering.
È anche per questo che l'editor di questo sito degrada con grazia sui file grandi — non perché tecnicamente non possa renderizzarli, ma perché renderizzarli sarebbe inutilizzabile.
2. Perché viene attaccato
Come funziona l'iniezione di __proto__?
Inquinamento del prototipo. Un attaccante costruisce JSON con una chiave __proto__; se il tuo codice lo unisce in un oggetto senza filtrare,
sovrascrive Object.prototype e influenza ogni oggetto nel runtime.
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Dangerous code: recursive merge without checking key names
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 — every object has been polluted
Il fatto chiave: JSON.parse stesso non inquina il prototipo; {"__proto__": ...} si analizza come una normale proprietà propria.
Ciò che è pericoloso è il merge, l'assegnazione, Object.assign, lodash.merge e operazioni simili dopo il parsing.
Tre difese:
- Filtra i nomi delle chiavi: salta
__proto__,constructoreprototypedurante il merge; - Usa
Object.create(null)come contenitore così non c'è una catena di prototipi da inquinare; - Usa una
Mapper memorizzare coppie chiave-valore non fidate.
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;
}
Nota anche che for...in percorre le proprietà ereditate; Object.keys() è più sicuro.
A cosa devo fare attenzione quando analizzo JSON non fidato?
Fallo in questo ordine — basso costo, alto rendimento:
- Limita la dimensione: rifiuta subito qualsiasi cosa oltre la soglia, senza prima leggerla in memoria;
- Limita la profondità: un JSON annidato centinaia di livelli può far overflow dello stack di un parser ricorsivo;
- Limita il numero di chiavi: previene attacchi di collisione hash e amplificazione della memoria;
- Filtra le chiavi pericolose:
__proto__,constructor,prototype; - Valida la struttura: passala attraverso uno Schema per confermare tipi e campi obbligatori;
- Non fidarti del
Content-Type: il tipo dichiarato e il contenuto effettivo possono non coincidere.
Un controllo di profondità è economico; puoi usare un filtro regex grossolano prima del parsing, oppure attraversare dopo il 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;
}
Ma questo codice è esso stesso ricorsivo e può comunque andare in overflow dello stack su dati maliziosamente profondi. L'approccio sicuro è applicare il limite durante il parsing,
usando un parser che supporti un'opzione di profondità (alcune librerie forniscono un parametro maxDepth).
Come avviene l'XSS tramite JSON?
JSON stesso non è HTML e non viene eseguito. L'XSS avviene nel punto in cui "un valore JSON viene concatenato in HTML".
Tre percorsi comuni:
| Percorso | Esempio | Difesa |
|---|---|---|
| Concatenazione nel template lato server | res.send('<div>' + data.name + '</div>') |
Escape HTML in output |
innerHTML lato frontend |
el.innerHTML = data.html |
Usa textContent o sanitizza |
| Incorporamento in uno script inline | <script>var d = ${JSON.stringify(data)}</script> |
Fai escape di <, >, & |
Il terzo è il più sottile. JSON.stringify non fa escape di <, quindi dati che contengono </script><script>alert(1)</script>
possono chiudere anticipatamente il tag script:
const data = { name: '</script><script>alert(1)</script>' };
// Output: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>
La correzione è convertire i caratteri pericolosi in 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');
}
Devo limitare la profondità di annidamento?
Sì. È una delle difese con il miglior rapporto costo-beneficio.
Le conseguenze del non limitarla: le funzioni ricorsive vanno in overflow dello stack, JSON.parse va in crash su alcune implementazioni quando la profondità è eccessiva,
e la serializzazione fa esplodere lo stack allo stesso modo. Un [[[[...]]]] da 1 KB basta a mandare in crash un parser non protetto.
Una soglia ragionevole dipende dal business:
| Tipo di dati | Limite suggerito |
|---|---|
| Oggetti di business ordinari | 10–20 livelli |
| Strutture ad albero (organigrammi, directory) | 50–100 livelli |
| Gateway API generico | 20 livelli |
Che cos'è il dirottamento JSON?
Una vulnerabilità storica: i primi browser consentivano a <script src> cross-origin di caricare ed eseguire JSON, e gli attaccanti rubavano dati ridefinendo
il costruttore Array o Object.prototype.__defineSetter__.
Le protezioni moderne sono abbastanza complete:
- I browser non consentono più di dirottare i letterali array con
__defineSetter__; - Un corretto
Content-Type: application/jsonimpedisce l'esecuzione dello script; X-Content-Type-Options: nosniffimpedisce il MIME sniffing.
I sistemi più vecchi portano ancora un rischio residuo. Consiglio pratico: usa un oggetto al livello superiore, non un array nudo.
Un wrapper come {"data": [...]} impedisce che la risposta sia un'istruzione JavaScript valida ({ viene trattato come blocco di codice),
bloccando il dirottamento a livello di sintassi.
Quali sono i rischi di registrare JSON nei log?
Tre:
- Informazioni sensibili scritte su disco: numeri di telefono, numeri di documento, token e password in chiaro finiscono nel sistema di logging, che di solito ha permessi più lassi e conservazione più lunga;
- Iniezione nei log: un valore JSON che contiene newline e prefissi di log falsificati divide un record in diversi e inquina la traccia di audit;
- Esplosione della dimensione: scrivere un'intera risposta da 10 MB nel log riempie rapidamente il sistema di logging.
Approccio:
// Redact and truncate before 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;
}
I sistemi di logging strutturato (come pino) fanno da soli l'escape dei newline, ma redazione e troncamento restano compito tuo.
3. Riferimento rapido
| Sintomo | Cosa controllare prima | Correzione |
|---|---|---|
| Parsing lento | Volume dei dati, se è coinvolto un deep clone | Suddivisione/streaming, rimuovi JSON.parse(JSON.stringify()) |
| La pagina si blocca | Task lunghi sul thread principale, numero di nodi renderizzati | Worker + scorrimento virtuale |
| La scheda va in crash | Uso della memoria | Non caricare tutto; passa allo streaming |
| Il deep clone perde dati | Date, NaN, Map |
Usa structuredClone |
| Gli oggetti hanno improvvisamente proprietà in più | Inquinamento del prototipo | Filtra __proto__, usa una Map |
| Compare uno script | Stringhe concatenate in HTML | Fai escape di <, usa textContent |
| Overflow dello stack | Profondità di annidamento | Limita a 10–20 livelli |
| I log si riempiono | Viene registrata l'intera risposta | Tronca + redigi |