Parsování, vykreslování a streamování velkého JSON
Shrnutí: Parsování často není úzkým hrdlem; vykreslování a kopírování ano. Optimalizujte podél čtyř vrstev „parse → traverse → render → transport“, aby vaše úsilí nepřišlo nazmar.

Stručně: nejprve změřte zvlášť parsování, průchod, vykreslování a přenos, poté postupujte v tomto pořadí – snižte počet kopií, rozbalujte na vyžádání s virtuálním scrollováním, přesuňte práci do Workeru, přejděte ke streamování a teprve potom změňte formát. Streamování má cenu jen tehdy, když se soubor nevejde do paměti, nebo když potřebujete malou podmnožinu jeho polí.
Závěr „JSON.parse je pomalý“ je v osmi z deseti případů špatný. Skutečně pomalé je to, co děláte po parsování.
1. Nejdříve měřte, poté optimalizujte
Pro stejný 10MB soubor JSON se čas strávený v různých fázích liší o řády:
| Fáze | Typická doba | Hlavní náklad |
|---|---|---|
JSON.parse (nativní) |
Desítky až něco přes sto milisekund | Jediné lineární skenování plus stavba objektů |
Hluboká kopie (structuredClone / rekurzivní) |
Možná 1–3násobek parsování | Znovupostavení celého grafu objektů |
| Hluboké srovnání / diff | Možná znovu dvojnásobek | Rekurzivní průchod plus porovnání řetězců |
| Vykreslení do DOM stromu | Zamrzne, jakmile je uzlů desítky tisíc | Layout a reflow |
| Spojování řetězců a poté serializace | Úměrné velikosti výstupu | Zdvojnásobení paměti |
Takže než začnete jednat, měřte: v prohlížeči obalte každou fázi pomocí performance.now(), nebo použijte console.time;
v Node sledujte růst haldy pomocí process.memoryUsage(). Bez baseline není optimalizace.
2. Vrstva parsování: tři strategie
2.1 Úplné parsování (výchozí volba)
const data = JSON.parse(text);
Nativní implementace provádí skenování a stavbu objektů v C++, takže čistě JS knihovna ji stěží překoná. Používejte ji, dokud platí tyto tři podmínky:
- Text se celý vejde do paměti (obvykle < 100 MB);
- Většina polí je po parsování skutečně potřeba;
- Míra nedůvěryhodnosti dat je zvládnutelná.
2.2 Streamované inkrementální parsování
Pomocí SAX-style parseru, jako je stream-json, můžete vyskenovat cílová pole, aniž byste postavili kompletní strom objektů:
import { createReadStream } from 'node:fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';
const pipeline = createReadStream('huge.json')
.pipe(parser())
.pipe(pick({ filter: 'items' }))
.pipe(streamArray());
for await (const { value } of pipeline) {
// Zpracovávejte jeden prvek najednou; paměť nezávisí na počtu prvků
handleItem(value);
}
Cena:
- Složitost kódu výrazně roste a zpracování chyb je nepořádnější;
- Je možný jen sekvenční přístup, žádný náhodný;
- Je nepřátelský k požadavkům vyžadujícím globální agregaci.
Rozhodovací pravidlo: streamování má cenu jen tehdy, když se soubor nevejde, nebo když potřebujete jen velmi malou podmnožinu jeho polí.
2.3 Změňte formát
Pokud jsou data „dávkou záznamů“, nikoli „jediným dokumentem“, měli jste od začátku používat JSON Lines. Řádkově oddělený formát mění „streamování“ na „čtení řádek po řádku“, čímž snižuje implementační složitost o řád.
3. Vrstva průchodu: jedna kopie méně vyhraje polovinu boje
// Anti-vzor: nejprve hluboká kopie „pro bezpečí“
const safeCopy = structuredClone(bigData); // Paměť se okamžitě zdvojnásobí
// Dobrý vzor: ve scénářích jen pro čtení předávejte reference přímo
function renderSummary(data: OrderDocument): Summary {
return { total: data.items.length, amount: data.total };
}
Časté skryté kopie:
JSON.parse(JSON.stringify(x))použité jako „hluboká kopie“ – to je nejdražší druh;{ ...data }kopíruje jen jednu úroveň, ale u velkého objektu stále kopíruje referenční pole všech klíčů nejvyšší úrovně;Array.prototype.mapvytváří nové pole, což je u mnoha prvků také nákladné;- Reaktivní proxy frameworku (Vue
reactive) proxi velké objekty rekurzivně a režie může být vyšší než samotné parsování.
Praktická rada: držte „surová data“ jako obyčejný imutabilní objekt a neobalujte je do reaktivity; odvozená data, která potřebujete zobrazit, počítejte a cachujte zvlášť.
4. Vrstva vykreslování: necpěte celý strom do DOM
5MB soubor JSON má zhruba 30 000 až 100 000 uzlů; vykreslit je všechny jako DOM elementy přestane prohlížeč reagovat.
Tři použitelné přístupy:
4.1 Rozbalování na vyžádání
Vykreslete jen přímé potomky kořenového uzlu a další úroveň počítejte až při rozbalení uzlů. To je standardní přístup stromových prohlížečů:
type TreeNode = { key: string; value: unknown; expanded: boolean };
/**
* Vrací jen aktuálně viditelné uzly a vyhýbá se stavbě celého stromu najednou.
* @param rootNode Kořenový uzel
* @param expandedPaths Množina cest, které byly rozbaleny
* @returns Seznam viditelných uzlů
*/
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
const visibleNodes: TreeNode[] = [];
/**
* Rekurzivně shromažďuje podřízené uzly rozbalených větví.
* @param currentNode Aktuální uzel
* @param currentPath Aktuální cesta
*/
const walk = (currentNode: TreeNode, currentPath: string): void => {
visibleNodes.push(currentNode);
if (!expandedPaths.has(currentPath) || typeof currentNode.value !== 'object' || currentNode.value === null) {
return;
}
for (const [childKey, childValue] of Object.entries(currentNode.value)) {
walk({ key: childKey, value: childValue, expanded: false }, `${currentPath}/${childKey}`);
}
};
walk(rootNode, '');
return visibleNodes;
}
4.2 Virtuální scrollování
Jakmile rozbalené uzly přesáhnou několik stovek, vykreslujte jen několik desítek ve viewportu a zbytek rezervujte zástupnými výškami. Nejúčinnější je u seznamových zobrazení (tabulky, logy).
4.3 Zobrazujte jen souhrny
Nejprve prezentujte „struktura + typ + počet“ (například items: Array(12000)); konkrétní hodnotu načtěte až když uživatel položku otevře.
Tento trik funguje nejlépe ve scénáři „chci jen potvrdit, že pole jsou správná“.
5. Hlavní vlákno: přesuňte práci do Workeru, když máte
Parsování, formátování a diff čehokoli většího než 1 MB by mělo jít do Web Workeru:
// json.worker.ts
self.onmessage = (event: MessageEvent<{ text: string }>) => {
try {
const parsed = JSON.parse(event.data.text);
self.postMessage({ ok: true, data: parsed });
} catch (parseError) {
self.postMessage({ ok: false, message: (parseError as Error).message });
}
};
/**
* Parsuje JSON uvnitř Workeru, aby hlavní vlákno nebylo zablokované.
* @param text Text JSON
* @returns Výsledek parsování
*/
function parseInWorker(text: string): Promise<unknown> {
return new Promise((resolvePromise, rejectPromise) => {
const worker = new Worker(new URL('./json.worker.ts', import.meta.url), { type: 'module' });
worker.onmessage = (event) => {
worker.terminate();
if (event.data.ok) {
resolvePromise(event.data.data);
} else {
rejectPromise(new Error(event.data.message));
}
};
worker.postMessage({ text });
});
}
Sledujte komunikační náklady Workers: postMessage ve výchozím nastavení používá strukturované klonování a klonování 10MB objektu jednou také trvá desítky milisekund.
Pokud potřebujete jen „parsovat a předat hlavnímu vlákně“, dělejte těžkou práci uvnitř Workeru (parse + transform + validace) a posílejte zpět shrnutí výsledku.
6. Vrstva přenosu: optimalizace velikosti
| Technika | Typický přínos | Cena |
|---|---|---|
| gzip / brotli | Komprimovaný výstup je často 10–25 % původní velikosti | Vyžaduje podporu serveru |
| Odstranění odsazení a prázdných znaků | Zmenšení o 20–40 % | Není čitelné pro lidi |
| Zkracování názvů polí | Znatelný přínos u hlubokého vnoření | Nižší čitelnost; vyžaduje mapovací tabulku |
| Čísla místo výčtových řetězců | Malý | Vyžaduje slovník |
| Stránkování / pole na vyžádání | Největší | Redesign API |
Pro stránky určené lidem je správná kombinace: API posílá kompaktní komprimovaný JSON a UI ho zkrášluje jen pro zobrazení. Mnoho nástrojových webů ve výchozím nastavení posílá frontendu znovu zkrášlený text a zbytečně posílá o 30 % bajtů víc.
7. Jak benchmarkovat
/**
* Měří medián doby trvání synchronní rutiny, aby jeden rozkolísaný běh nezkreslil výsledek.
* @param runOnce Testovaná funkce
* @param repeatCount Počet opakování
* @returns Medián doby trvání v milisekundách
*/
function measureMedian(runOnce: () => void, repeatCount = 9): number {
const durations: number[] = [];
for (let roundIndex = 0; roundIndex < repeatCount; roundIndex += 1) {
const startedAt = performance.now();
runOnce();
durations.push(performance.now() - startedAt);
}
durations.sort((firstValue, secondValue) => firstValue - secondValue);
return durations[Math.floor(durations.length / 2)];
}
Tři pravidla disciplíny:
- Používejte pevný vzorek a nikdy negenerujte data náhodně, jinak čísla nelze srovnávat;
- Spusťte alespoň 9krát a vezměte medián; první běh zahrnuje zahřívání JIT a musí se zahodit;
- Zaznamenejte běhové prostředí (verze Node, CPU), jinak jsou srovnání mezi stroji nesmyslná.
8. Kontrolní seznam
- [ ] Existují baseline čísla, aby šlo srovnat před a po;
- [ ] Parsování čehokoli většího než 1 MB běží ve Workeru;
- [ ] Stromová zobrazení rozbalují na vyžádání a seznamy používají virtuální scrollování;
- [ ] Žádné zbytečné hluboké kopie ani reaktivní obalování;
- [ ] Když jsou data „dávkou záznamů“, používá se JSON Lines;
- [ ] API posílá kompaktní JSON a formátování probíhá jen pro zobrazení.