FAQ k výkonu a bezpečnosti: pomalá, trhaná a nedůvěryhodná data
Shrnutí: Proč se soubor JSON o desítkách MB neotevře, kolik stojí hluboké klonování, jak zabránit znečištění __proto__ a kudy proniká XSS – deset závěrů první pomoci seřazených podle příznaku.

Stručně: nejprve diagnostikujte podle příznaku – parsování, vykreslování, nebo paměť – a poté projděte pořadí oprav: snižte objem dat, přestaňte hluboce klonovat, přesuňte parsování do Workeru s virtuálním scrollováním a cokoli nad 50 MB streamujte. U nedůvěryhodného vstupu omezte velikost a hloubku vnoření dřív, než odfiltrujete __proto__, constructor a prototype.
„JSON je pomalý“ je v devíti z deseti případů nepřesné; skutečně pomalé je obvykle vykreslování, hluboké klonování nebo špatné pořadí operací.
1. Proč je to pomalé
Parsování velkého souboru JSON je pomalé. Jak ho optimalizovat?
Nejprve lokalizujte úzké hrdlo, o optimalizaci mluvte až potom. Fáze se liší o několik řádů:
| Fáze | Hrubý čas pro 10 MB dat | Proč je to úzké hrdlo |
|---|---|---|
| Přenos po síti | Závisí na šířce pásma | Nekomprimovaný JSON se obvykle zkomprimuje na asi 1/5 |
JSON.parse |
20–80 ms | Nativní implementace, už rychlá |
| Rekurzivní průchod / hluboký klon | 200–2000 ms | Čistě JS zpracování uzel po uzelm |
| Vykreslování DOM (stromové zobrazení) | Sekundy, snadno zamrzne | Počet uzlů exploduje |
Pořadí optimalizace by tedy mělo být:
- Snižte objem dat: nechte API stránkovat, vracejte jen potřebná pole a zapněte gzip/brotli;
- Vyhněte se hlubokému klonování: viz další část; to je nejčastější skrytý náklad;
- Líné vykreslování: ve stromovém zobrazení vykreslujte jen rozbalené úrovně, místo abyste najednou generovali desítky tisíc DOM uzlů;
- Streamování: neparsujte všechno najednou, když data přesáhnou 10 MB.
Pokud je úzkým hrdlem skutečně JSON.parse, znamená to, že data už mají desítky MB nebo více, a správným krokem je streamovaný parser:
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
// Zpracovávejte prvky pole jeden po druhém; spotřeba paměti nezávisí na celkové velikosti
const pipeline = fs.createReadStream('big.json')
.pipe(parser())
.pipe(streamArray());
pipeline.on('data', ({ value }) => {
processItem(value);
});
JSON o desítkách MB zhroutí prohlížeč ve chvíli, kdy se otevře. Co teď?
Nejprve potvrďte, zda „havaruje“, nebo „zamrzne“. Oba případy vyžadují zcela odlišné zacházení:
- Zamrznutí: záložka ukazuje „Page unresponsive“ a po desítkách sekund se může zotavit. Příčinou jsou dlouhé úlohy, které zahltí hlavní vlákno;
- Havárie: záložka se změní na „Aw, Snap“, obvykle vyčerpání paměti (OOM).
V obou případech to nezpracovávejte v hlavním vlákně. Přesuňte parsování do Web Workeru:
// worker.js
self.onmessage = (event) => {
const parsed = JSON.parse(event.data);
self.postMessage(parsed);
};
Worker řeší „UI nezamrzne“, nikoli paměťový strop. Záložka prohlížeče má obvykle k dispozici 1–4 GB; 200MB JSON parsovaný do objektů může zabrat 1 GB nebo více (každá dvojice klíč-hodnota nese režii objektu), a s vykreslováním navrch snadno narazí na limit.
Skutečným řešením je nikdy nenačítat všechno:
| Velikost dat | Přístup |
|---|---|
| < 5 MB | Parsujte přímo pomocí JSON.parse, sledujte vykreslování |
| 5–50 MB | Parsování ve Workeru + virtuální scrollování |
| > 50 MB | Streamované parsování, ponechte jen potřebná pole |
| > 500 MB | Změňte formát (Parquet / SQLite / rozdělený JSON Lines) |
Je JSON.parse(JSON.stringify(obj)) hluboký klon?
Je to hluboký klon, ale na několika místech „tiše mění data“, takže se nehodí jako obecný nástroj pro klonování.
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 se stane řetězcem
// nan: null, // NaN se stane null
// nested: { a: 1 } // správná je jen tato úroveň
// }
// fn a undef zmizí, map se stane {}
Seznam problémů:
| Typ | Výsledek |
|---|---|
Date |
Stane se řetězcem ISO |
NaN / Infinity |
Stane se null |
undefined / funkce / Symbol |
Uvnitř objektů zmizí, uvnitř polí se stanou null |
Map / Set |
Stane se {} |
BigInt |
Vyhodí TypeError |
| Kruhové reference | Vyhodí TypeError |
Moderní prohlížeče mají structuredClone, který podporuje Date, Map, Set i kruhové reference, ale ne funkce:
const copy = structuredClone(source);
Pokud jde jen o mělké sloučení konfigurace, nejlevnější je operátor spread: { ...config, timeout: 3000 }.
Proč je rozbalování stromového zobrazení tak trhané?
Protože každý uzel je DOM element a jakmile počet uzlů dosáhne desítek tisíc, náklady na layout prohlížeče explodují.
10MB JSON může obsahovat 50k–100k listových uzlů. I když vykreslujete jen viditelnou oblast, jednorázová konstrukce DOM trvá stovky milisekund až sekundy a každé následné scrollování spustí reflow.
Správný přístup:
- Virtuální scrollování: vykreslujte jen několik desítek řádků ve viewportu a při scrollování znovu používejte DOM uzly;
- Ve výchozím stavu sbaleno: rozbalte jen první dvě úrovně a hlubší úrovně vykreslujte na vyžádání;
- Omezte rozsah vykreslování: nad 10k uzlů zobrazte „Data too large, use search to locate it“, místo abyste vynucovali vykreslení.
To je také důvod, proč editor na tomto webu u velkých souborů degraduje elegantně – ne proto, že by je technicky neuměl vykreslit, ale proto, že by jejich vykreslení bylo nepoužitelné.
2. Proč je napadán
Jak funguje injektáž __proto__?
Prototype pollution. Útočník zkonstruuje JSON s klíčem __proto__; pokud ho váš kód sloučí do objektu bez filtrování,
přepíše Object.prototype a ovlivní každý objekt v běhovém prostředí.
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Nebezpečný kód: rekurzivní sloučení bez kontroly názvů klíčů
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 — každý objekt byl znečištěn
Klíčový fakt: JSON.parse sám prototyp neznečišťuje; {"__proto__": ...} se parsuje do obyčejné vlastní vlastnosti.
Nebezpečné je slučování, přiřazování, Object.assign, lodash.merge a podobné operace po parsování.
Tři obrany:
- Filtrujte názvy klíčů: při slučování přeskakujte
__proto__,constructoraprototype; - Použijte
Object.create(null)jako kontejner, aby neexistoval řetězec prototypů ke znečištění; - Použijte
Mappro ukládání nedůvěryhodných dvojic klíč-hodnota.
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;
}
Pozor také, for...in prochází zděděné vlastnosti; Object.keys() je bezpečnější.
Na co si dát pozor při parsování nedůvěryhodného JSON?
Dělejte to v tomto pořadí – nízké náklady, vysoký přínos:
- Omezte velikost: cokoli nad prahem okamžitě odmítněte, aniž byste to nejprve načetli do paměti;
- Omezte hloubku: JSON vnořený stovky úrovní hluboko může přetéci zásobník rekurzivního parseru;
- Omezte počet klíčů: brání útokům přes hash kolize a paměťové amplifikaci;
- Filtrujte nebezpečné klíče:
__proto__,constructor,prototype; - Validujte strukturu: prožeňte ji Schema, abyste potvrdili typy a povinná pole;
- Nevěřte
Content-Type: deklarovaný typ a skutečný obsah se mohou rozcházet.
Kontrola hloubky je levná; před parsováním můžete použít hrubý filtr regulárním výrazem, nebo projít po parsování:
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;
}
Ale tento kód je sám rekurzivní a u zlovolně hlubokých dat může stále přetéci zásobník. Bezpečný přístup je vynutit limit během parsování,
pomocí parseru, který podporuje volbu hloubky (některé knihovny poskytují parametr maxDepth).
Jak vzniká XSS přes JSON?
JSON sám není HTML a nevykonává se. XSS vzniká v místě, kde se „hodnota JSON spojí do HTML“.
Tři časté cesty:
| Cesta | Příklad | Obrana |
|---|---|---|
| Spojování v serverové šabloně | res.send('<div>' + data.name + '</div>') |
HTML-escape na výstupu |
Frontendové innerHTML |
el.innerHTML = data.html |
Použijte textContent nebo sanitizaci |
| Vložení do inline skriptu | <script>var d = ${JSON.stringify(data)}</script> |
Escapujte <, >, & |
Třetí je nejjemnější. JSON.stringify neescapuje <, takže data obsahující </script><script>alert(1)</script>
mohou uzavřít značku skriptu předčasně:
const data = { name: '</script><script>alert(1)</script>' };
// Výstup: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>
Oprava je převést nebezpečné znaky na Unicode escape sekvence:
function safeJsonForHtml(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
}
Mám omezit hloubku vnoření?
Ano. Je to jedna z obran s nejlepším poměrem nákladů a přínosu.
Následky neomezení: rekurzivní funkce přetékají zásobník, JSON.parse v některých implementacích při přílišné hloubce havaruje
a serializace přetéká zásobník stejně. 1KB [[[[...]]]] stačí na to, aby srazilo nechráněný parser.
Rozumný práh závisí na obchodu:
| Typ dat | Doporučený limit |
|---|---|
| Obyčejné obchodní objekty | 10–20 úrovní |
| Stromové struktury (organizační schémata, adresáře) | 50–100 úrovní |
| Obecná API brána | 20 úrovní |
Co je JSON hijacking?
Historická zranitelnost: rané prohlížeče umožňovaly cross-origin <script src> načíst a vykonat JSON a útočníci kradli data přepsáním
konstruktoru Array nebo Object.prototype.__defineSetter__.
Moderní ochrany jsou poměrně kompletní:
- Prohlížeče už nedovolují unést pole literálů pomocí
__defineSetter__; - Správný
Content-Type: application/jsonbrání vykonání skriptu; X-Content-Type-Options: nosniffbrání sniffování MIME.
Starší systémy stále nesou reziduální riziko. Praktická rada: používejte na nejvyšší úrovni objekt, nikoli samotné pole.
Obálka jako {"data": [...]} brání tomu, aby odpověď byla platným příkazem JavaScriptu ({ se považuje za blok kódu),
čímž hijacking blokuje na úrovni syntaxe.
Jaká rizika má logování JSON?
Tři:
- Citlivé informace zapsané na disk: telefonní čísla, rodná čísla, tokeny a hesla v prostém textu přistanou v logovacím systému, který obvykle má volnější oprávnění a delší uchovávání;
- Injektáž logů: hodnota JSON obsahující zalomení řádků a zfalšované prefixy logu rozdělí jeden záznam na několik a znečistí auditní stopu;
- Exploze velikosti: zápis celé 10MB odpovědi do logu rychle zaplní logovací systém.
Přístup:
// Před logováním zredigujte a zkraťte
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;
}
Strukturované logovací systémy (například pino) escapují zalomení řádků samy, ale redakce a zkrácení je stále váš úkol.
3. Rychlý přehled
| Příznak | Zkontrolujte nejprve | Oprava |
|---|---|---|
| Pomalé parsování | Objem dat, zda se neúčastní hluboké klonování | Dávkování/streamování, odstraňte JSON.parse(JSON.stringify()) |
| Stránka zamrzne | Dlouhé úlohy v hlavním vlákně, počet vykreslených uzlů | Worker + virtuální scrollování |
| Záložka havaruje | Spotřeba paměti | Nenačítejte všechno; přejděte ke streamování |
| Hluboký klon ztrácí data | Date, NaN, Map |
Použijte structuredClone |
| Objekty najednou mají nadbytečné vlastnosti | Prototype pollution | Filtrujte __proto__, použijte Map |
| Vyskočí skript | Řetězce spojené do HTML | Escapujte <, použijte textContent |
| Přetečení zásobníku | Hloubka vnoření | Omezte na 10–20 úrovní |
| Logy se zaplní | Loguje se celá odpověď | Zkrácení + redakce |