JSON Toolkit

Proč velké stromy JSON zadrhávají: virtuální scrollování a strategie sbalování

Shrnutí: Trhání začíná při vytváření DOM uzlů, nikoli při scrollování. Pokrývá vykreslování se sbalením předem, virtuální seznamy, zvýrazňování hledání a hranice Workeru, s metrikami.

Pokročilý~10 min čteníPublikováno 2026-10-31Aktualizováno 2026-10-31
Virtuální scrollování a strategie sbalování pro prohlížeč JSON

Stručně: rozdělte „pomalé“ na parsování, vytváření uzlů, layout a paint a interakci a poté každou fázi změřte. Implementujte v tomto pořadí – ve výchozím stavu sbalené, poté přidejte virtuální seznam, přesuňte výpočty do Workeru a omezujte zvýrazňování na viditelné řádky. Skočit rovnou na virtuální seznam bez sbalení jen zmenší viditelnou množinu; přepočet celého stromu vás stále stojí.

Trhání obvykle pochází ze stovek tisíc DOM uzlů, které jste vytvořili pro těch 5 MB, nikoli z těch 5 MB samotných.

1. Rozdělte „pomalé“ na čtyři fáze

Když uživatel řekne „při otevření to zamrzne“, problém může žít ve čtyřech zcela odlišných fázích, každá s jiným řešením:

Fáze Typický spouštěč Vnímatelný práh Dominantní náklad
Parsování a stavba objektů Otevření souboru, vložení textu Jakákoli úloha přes 50 ms Skenování řetězců, stavba grafu objektů
Vytváření DOM uzlů Vykreslení celého stromu najednou Desítky tisíc uzlů Alokace elementů, textových uzlů, stylových objektů
Layout a paint První zobrazení, scrollování Jakýkoli snímek přes 16,7 ms Geometrie, reflow, repaint
Reakce na interakci Rozbalení, sbalení, hledání Přes 100 ms působí zpožděně Znovusploštění a znovuvykreslení

Hlavní vlákno prohlížeče dělá jednu věc najednou. Pokud kterákoli fáze zabírá víc než 50 ms, scrollování a psaní ztrácejí snímky; několik takových úloh v řadě a stránka vypadá mrtvá. První optimalizace tedy není requestAnimationFrame; je to měření, která fáze skutečně spotřebovává čas.

2. Počet uzlů je skutečný nepřítel

Stromové prohlížeče násobí počty uzlů už ze své podstaty. Jediný řádek JSON se obvykle stane „odsazení + klíč + dvojtečka + hodnota + čárka + šipka“, takže konzervativní odhad je 3 až 6 DOM uzlů na řádek:

Velikost souboru Přibližné řádky DOM uzlů stromového zobrazení (odhad) Jak vypadá jednorázové vykreslení
100 KB 2 500 10 000 Obvykle snesitelné
1 MB 25 000 100 000 Zřetelně trhané, možná sekundy prázdné obrazovky
5 MB 125 000 500 000 Velmi pravděpodobně neodpovídá
20 MB 500 000 Přes 2 miliony Zhroucení paměti i layoutu

Každý element je víc než několik desítek bajtů objektu: nese stylová pravidla, layout box a instrukce pro paint. Jakmile je uzel vytvořen, stále stojí práci layoutu a stylů, i když je později skryt sbaleným rodičem, dokud nebyl skutečně odstraněn. „Vykreslit všechno a poté skrýt“ není optimalizace; jen odkládá náklad na příští reflow.

3. Nejprve sbalit, poté virtualizovat

Virtuální scrollování řeší „příliš mnoho viditelných řádků“. Sbalování řeší „těchto řádků bychom vůbec neměli potřebovat tolik vidět“. Pořadí záleží: u dokumentu s milionem řádků rozbaleného ve výchozím stavu musí i dokonalý virtuální seznam přepočítat celý strom, když uživatel klikne na „sbalit vše“.

Použitelná strategie sbalování:

  • Ve výchozím stavu rozbalte jen první dvě úrovně. Kořen a jeho přímá pole jsou viditelné; hlubší úrovně ukazují shrnutí jako items: Array(12000);
  • Do shrnutí dejte typ a počet. Uživatelé kontrolující „jsou názvy polí správné“ zřídka potřebují deset tisíc záznamů;
  • Cachujte řádky pro každý podstrom. Každý rozbalený uzel vlastní své zploštělé pole řádků; sbalení ho zahodí a rozbalení přepočítá jen ten podstrom;
  • Nikdy neskenujte celý strom při každém přepnutí. Pokud rozbalení nastane na řádku 3 000, nahraďte jen ten rozsah a okolní řádky nechte nedotčené.
/**
 * Vloží řádky podstromu za jeho rodiče, místo aby znovu splošťoval celý strom.
 * @param rows Aktuální pole viditelných řádků
 * @param parentIndex Index řádku rodiče
 * @param childRows Zploštěné řádky podstromu
 * @returns Nové pole viditelných řádků
 */
function replaceSubtreeRows(
  rows: TreeRow[],
  parentIndex: number,
  childRows: TreeRow[],
): TreeRow[] {
  const nextRows = rows.slice();
  nextRows.splice(parentIndex + 1, 0, ...childRows);
  return nextRows;
}

Vzor, kterému se vyhnout, je „jedno rozbalení, jeden plný relayout“. Nové procházení celého grafu objektů při každém kliknutí činí náklad lineárně rostoucí s dokumentem. S cachováním po podstromu stojí jedno rozbalení jen tolik co ten podstrom.

4. Virtuální seznamy: vykreslujte jen viewport

Jakmile sbalení snížilo počet řádků, zbytek může stále být desítky tisíc řádků. Virtuální seznam nastaví výšku kontejneru na „celkový počet řádků × pevná výška řádku“ a vytváří elementy jen pro viewport plus malý buffer nad a pod.

const ROW_HEIGHT = 24;
const OVERSCAN = 8;

/**
 * Vypočítá rozsah řádků, které má viewport vykreslit.
 * @param rows Všechny viditelné řádky
 * @param scrollTop Aktuální offset scrollování kontejneru
 * @param viewportHeight Viditelná výška kontejneru
 * @returns Řádky k vykreslení, počáteční index a vertikální offset
 */
function getViewportRows(
  rows: TreeRow[],
  scrollTop: number,
  viewportHeight: number,
): { visibleRows: TreeRow[]; startIndex: number; offsetTop: number } {
  const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
  const end = Math.min(
    rows.length,
    Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) + OVERSCAN,
  );

  const visibleNodes = rows.slice(start, end);
  return {
    visibleRows: visibleNodes,
    startIndex: start,
    offsetTop: start * ROW_HEIGHT,
  };
}

Nepodkročitelná omezení:

  • Výška řádku musí být pevná. Jen tehdy vám scrollTop / ROW_HEIGHT dá index přímo. Povolení zalamovaných řádků vás nutí měřit a cachovat každou výšku, což přidává řád složitosti a chvění;
  • Offsetujte pomocí transform nebo absolutního pozicování. Netlačte obsah dolů pomocí spacer elementu, jinak každé scrollování mutuje layout;
  • Používejte stabilní klíče. Klíčujte řádky podle cesty, nikoli podle indexu pole, jinak rozbalení jednoho řádku remountuje každý následující;
  • Počítejte aritmetiku jen ve scroll handleru. Vykreslování odložte na requestAnimationFrame a chraňte se proti duplicitnímu plánování;
  • Držte buffer malý. Pět až deset řádků na každou stranu stačí; zdvojnásobení bufferu zdvojnásobí DOM uzly i práci se styly.

S 800pixelovým viewportem a 24pixelovými řádky je viditelných zhruba 34 řádků a i s bufferem vykreslíte asi padesát. Základní dokument může nést sto tisíc řádků, zatímco DOM trvale drží několik stovek elementů, čímž náklady na layout zůstávají ploché.

5. Sbalování, rozbalování a ukotvení scrollování

Jakmile se index řádku stane jedinou souřadnicí, několik detailů interakce se začne rozbíjet:

  1. Zachovejte vizuální kotvu při sbalování nad viewportem. Řádky níže se posunou nahoru; pokud se scrollTop neupraví ve stejném commitu, obsah vypadá, že skočil. Zaznamenejte cestu kotvového řádku a obnovte pozici scrollování pomocí jeho nového indexu.
  2. Před skokem rozbalte předky. Když hledání zasáhne hluboké pole, nejprve rozbalte všechny rodiče, poté vypočítejte index cílového řádku, pak scrollujte a zvýrazněte. Přeskočte první krok a cílový řádek v poli viditelných ani není.
  3. Navigaci klávesnicí řiďte z viditelných řádků. Nahoru a dolů posouvají index řádku; vlevo a vpravo rozbalují a sbalují. Nikdy neprocházejte celý strom uvnitř handleru klávesy.
  4. Dávkujte aktualizace do jednoho snímku. Při rozbalení několika úrovní v řadě sloučte změny pole a vykreslete jednou, jinak každá úroveň spustí plný reflow.

6. Hledání a zvýrazňování: spočítejte jednou, zobrazte málo

Hledání je nejčastěji přehlížená výkonová past ve stromovém prohlížeči. Pokud každý stisk klávesy prochází celý graf objektů a znovu staví seznam výsledků, dokument se sto tisíci uzlů okamžitě zamrzne.

Vrstvený přístup:

  • Debouncujte vstup na 150–250 ms a každé hledání označte pořadovým číslem, aby se zastaralé výsledky zahodily;
  • Skenujte ve Workeru. Procházení grafu objektů je čistý výpočet bez závislosti na DOM, takže nemá blokovat psaní;
  • Ukládejte jen cesty a indexy řádků. Nekopírujte každý nalezený uzel ani nepředstavějte zvýrazněné HTML;
  • Zvýrazňujte jen viditelné řádky. Viewport drží několik desítek řádků; styly aplikujte podle nalezené cesty při vykreslení;
  • Pro skoky znovu použijte „rozbalit předky, poté lokalizovat index“, stejnou cestu jako při kliknutí na šipku.

Jeden bezpečnostní detail: klíče a hodnoty ve stromovém prohlížeči jsou data uživatele. Stavět z nich zvýrazněné HTML vyžaduje escapování, jinak se řetězec obsahující značky stane XSS. Prohlížeč JSON nepotřebuje bohatý text, takže textové uzly plus názvy tříd jsou bezpečnější i rychlejší.

7. Zvýrazňování syntaxe: CodeMirror a Monaco jsou různé nástroje

Stromové zobrazení a textový editor jsou různé vykreslovací modely, takže „co je rychlejší“ není přímé srovnání:

Dimenze CodeMirror 6 Monaco Editor
Pozicování Modulární jádro editoru Plnohodnotný editor IDE
Vykreslování Podle viewportu Podle viewportu, ale modely a dekorace jsou těžší
Parsování Inkrementální parsování Lezerem nad změněnými rozsahy Monarch tokenizer, řádek po řádku
Velké soubory Závisí na rozšířeních; ve výchozím stavu lehký Plný funkcí, vyšší paměť a náklady na start
Skládání a hledání Samostatná rozšíření, načítaná na vyžádání Vestavěné, s trvalou režií
Typická past Příliš mnoho rozšíření obnoví náklady Minimap, sticky scroll a vrstvené dekorace

Dva praktické závěry:

  • Stromové zobrazení nepotřebuje plný tokenizer. Dejte klíčům, řetězcům, číslům a booleanům jednu barvu každému pomocí prostých textových uzlů a CSS tříd; to stojí o řád méně než spouštění zvýrazňování syntaxe;
  • Nedávejte stromové zobrazení a editor najednou. Udržování dvou modelů pro jeden dokument zdvojnásobuje paměť; staré zobrazení zničte, místo abyste ho skryli.

Pokud editor do prohlížeče skutečně patří, CodeMirror 6 se hodí pro „jen pro čtení, velký soubor, lokální zvýrazňování“, zatímco Monaco pro dokončování, diagnostiku a multi-kurzorové úpravy. Než začnete číst seznam funkcí, změřte dobu prvního vykreslení a rezidentní paměť. Hlubšímu srovnání editorů a CLI nástrojů se věnuje Editory, nástroje příkazového řádku a automatizace.

8. Hranice Workeru: přesuňte parsování a stavbu řádků mimo hlavní vlákno

Druhá polovina trhání vykreslování je parsování v hlavním vlákně. Pro JSON nad 1 MB patří parsování, formátování a stavba zploštěných řádků do Web Workeru; hlavní vlákno jen bere řádky a maluje viewport.

Workers nejsou zdarma: postMessage používá strukturované klonování, takže poslání 50MB grafu objektů zpět do hlavního vlákna staví druhou kopii a zdvojnásobuje vrchol. Správná hranice je parsovat a zplošťovat uvnitř Workeru, vracet jen řádky, které renderer potřebuje:

worker.onmessage = (event: MessageEvent<{ rows: TreeRow[] }>) => {
  rows = event.data.rows;
  requestAnimationFrame(() => renderViewport());
};

worker.postMessage({ text });

Pokud jsou data řádků stále velká, zkomprimujte řádky do paralelních polí (cesta, typ, offset hodnoty), umístěte je do ArrayBuffer a převeďte vlastnictví, aby hlavní vlákno neplatilo za kopii. Cenou je psaní vlastního serializátoru, což má cenu jen u milionů řádků.

Pravidlo pro hranici je jednoduché: hlavní vlákno má dělat jen práci, která se musí dotknout DOM. Parsování, hledání, řazení, formátování i diff se mohou přesunout. Vykreslování, scrollování, vstup a fokus zůstávají v hlavním vlákně a každý snímek se musí vejít do rozpočtu práce.

9. Metriky a pořadí implementace

Optimalizace nemůže skončit u „působí rychleji“. Následující sada může jít rovnou do kontrolního seznamu pro přijetí:

Metrika Cíl Jak měřit
První viditelný snímek Pod 300 ms performance.now() od otevření po první paint
Dlouhé úlohy Žádná přes 50 ms PerformanceObserver sledující longtask
Čas snímku při scrollování Trvale pod 16,7 ms Záznam v DevTools Performance
DOM elementů ve viewportu Pod 2 000 document.querySelectorAll('*').length
Reakce rozbalení / sbalení Pod 50 ms Časové značky kolem interakce
Hledání nad 100 000 řádky Pod 300 ms, uvnitř Workeru Čas ve Workeru a vrácená doba
Rezidentní halda Pod 3násobek velikosti textu Snapshot haldy v DevTools Memory

Měřte se skutečným souborem, nikoli uniformně generovanými malými objekty; tvar dat záleží a dlouhé řetězce, hluboké vnoření a velmi široká pole produkují různé křivky. Samotnému účtu grafu objektů se věnuje Účet paměti JSON.

Implementujte v tomto pořadí:

  1. Změřte baseline: doba prvního vykreslení, počet uzlů, čas snímku při scrollování;
  2. Ve výchozím stavu sbaleno: rozbalte dvě úrovně, ve shrnutích ukažte typ a počet;
  3. Virtuální seznam: pevná výška řádku, stabilní klíče, viewport plus malý buffer;
  4. Přesuňte výpočty: parsování, hledání a zplošťování do Workeru;
  5. Omezte zvýrazňování: stylujte jen viditelné řádky, vyhněte se znovuvykreslení celého stromu;
  6. Přidejte zábrany: nad prahem nabídněte „zobrazit prvních N řádků“ nebo vyžadujte filtr hledání.

Každý krok sdílí jeden cíl: snížit počet uzlů a objektů, které musí existovat ve stejném okamžiku, místo abyste žádali prohlížeč, aby na nich pracoval tvrději. Úplné pozadí vrstvené strategie je v Parsování, vykreslování a streamování velkého JSON.

Může virtuální scrollování opravit trhání při parsování?

Ne. Virtuální scrollování jen snižuje počet DOM uzlů. Pokud parsování stále probíhá v hlavním vlákně, stránka stále zadrhává. Přesuňte parsování do Workeru pro dokumenty nad 1 MB a oba problémy řešte odděleně.

Všechno je rozbalené a stále to zadrhává. Co je špatně?

Buď virtuální seznam není skutečně aktivní, nebo každé rozbalení znovu skenuje celý strom. Zkontrolujte, zda řádky mimo obrazovku stále vytvářejí DOM a zda rozbalení přepočítává jen postižený podstrom.

CodeMirror, nebo Monaco pro prohlížeč JSON?

Pro jen pro čtení, velké soubory a lokální zvýrazňování preferujte CodeMirror 6; jeho modulární načítání na vyžádání je lehčí. Monaco zvolte, když potřebujete dokončování, diagnostiku a multi-kurzorové úpravy. U obou se vyhněte mountování stromového zobrazení a editoru pro stejný dokument najednou.

Proč je pozice scrollování po skoku z hledání špatná?

Obvykle protože se zobrazení posunulo před rozbalením. Nejprve rozbalte všechny předky, poté nastavte pozici scrollování podle nového indexu řádku a zvýrazněte až nakonec. Klíčujte řádky podle cesty, nikoli podle indexu, aby rozbalení neposunulo každý index.

Časté dotazy

Jak zjistit, ze které fáze trhání pochází?

Rozdělte „pomalé“ na parsování a stavbu objektů, vytváření DOM uzlů, layout a paint a reakci na interakci, a každou fázi změřte pomocí performance.now() od otevření po první paint. Poté sledujte longtask pomocí PerformanceObserver – jakákoli úloha přes 50 ms ztrácí snímky a záznam v DevTools Performance ukáže, zda čas snímku při scrollování zůstává pod 16,7 ms.

Proč vykreslení celého stromu najednou ztmavne obrazovku?

5MB dokument má asi 125 000 řádků a stromové zobrazení obvykle mění každý řádek na 3 až 6 DOM uzlů – zhruba 500 000 elementů najednou. Každý element nese také stylová pravidla, layout box a instrukce pro paint, takže několik úloh hlavního vlákna přes 50 ms v řadě nechá stránku vypadat mrtvou. Uzly skryté sbaleným rodičem stále stojí práci layoutu, takže „vykreslit všechno a poté skrýt“ není optimalizace.

Mám poslat 50MB graf objektů zpět z Workeru?

Ne. postMessage používá strukturované klonování, takže přijímající strana staví druhou kopii a vrchol se zdvojnásobí. Správná hranice je parsovat a zplošťovat uvnitř Workeru a vracet jen řádky, které renderer potřebuje; u milionů řádků můžete zabalit paralelní pole (cesta, typ, offset hodnoty) do ArrayBuffer a převést vlastnictví.