JSON Toolkit

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.

Středně pokročilý~13 min čteníPublikováno 2026-10-30Aktualizováno 2026-10-31
Titulní obrázek článku Parsování, vykreslování a streamování velkého JSON

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.map vytváří 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:

  1. Používejte pevný vzorek a nikdy negenerujte data náhodně, jinak čísla nelze srovnávat;
  2. Spusťte alespoň 9krát a vezměte medián; první běh zahrnuje zahřívání JIT a musí se zahodit;
  3. 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í.