100 MB JSON-stresstest: Worker-doorvoer, Geheugenplafonds en Toggle-coalescentie

Door Aerisium Core · ·

Een 100 MB JSON-bestand zou een browsertab niet moeten laten crashen. De naïeve aanpak garandeert falen op deze schaal: parsen op de hoofdthread, debouncen van elke toggle, de volledige DOM in het geheugen houden. Maar het vervangen van elk van die patronen door een correct alternatief (Web Worker, coalescentie-referenties, DOM-virtualisatie) produceert een architectuur die worst-case invoer overleeft zonder zichtbare haperingen.

Dit artikel documenteert echte metingen van het laden en manipuleren van een 102 MB JSON-payload in een browser-gebaseerde JSON-editor.

Testomgeving

ParameterWaarde
CPUIntel Core i5-8250U @ 1,60 GHz (4 cores, 8 logisch)
RAM8 GB
OSWindows 10 22H2
BrowserChrome 149.0.7827.201
Payload102,67 MB JSON, 1.601.887 sleutels

De payload bestond uit 180.000 objecten, elk met een id-veld en een data-string van 400 tekens, verpakt in een array. Deze structuur bootst een grote log-dump of database-export na.

Initiële Formatting: Worker-doorvoer

Het bestand werd geladen via slepen en neerzetten. FileReader.readAsText() produceerde een ruwe string, die naar een Web Worker werd gestuurd voor parsen en formatteren. De handler van de worker werd geïnstrumenteerd met performance.now() om de parse-tijd van de stringify-tijd te isoleren:


const parseStart = performance.now();

parsedObj = JSON.parse(code);

parseTimeMs = performance.now() - parseStart;

const stringifyStart = performance.now();

result = JSON.stringify(transformed, null, indent);

stringifyTimeMs = performance.now() - stringifyStart;
FaseTijd
Totale worker-roundtrip2.463 ms
JSON.parse()581 ms
JSON.stringify() met inspringing409 ms

De resterende ~1,5 seconden werden verbruikt door sleuteltelling (recursieve doorloop van alle 1,6M sleutels) en een deep-clone-sorteerbewerking (sortObjKeys) wanneer sleutelsortering actief was.

De totale 2,5 seconden bevinden zich ruim onder de drempelwaarde “Pagina reageert niet” van de browser. De UI-thread bleef volledig responsief omdat alle zware berekeningen werden uitgevoerd in de achtergrondthread van de worker.

Geheugentoewijzing na Initiële Parse

Een heap-snapshot direct nadat de worker het resultaat retourneerde:

MetingWaarde
JS Heap (initiële formatting)232 MB
Totaal tab-geheugen647 MB

Een 102 MB-string expandeert tot ongeveer 4× de bestandsgrootte in het geheugen vanwege JavaScript’s UTF-16-codering. De resterende overhead omvat de toestandsboom van de editor, de geformatteerde uitvoerstring en V8’s interne objectrepresentatie van de geparste structuur. V8 wijst geheugen toe voor Hidden Classes, property-backing stores en element-backing stores voor elk object in de array. Elk van de 180.000 objecten draagt vaste structurele overhead bovenop de sleutel- en waarde-bytes.

Toggle-coalescentie: Synchronisatie-referentie

De naïeve tijdgebaseerde aanpak (een 50 ms trailing debounce) faalt omdat deze alleen vertraagt — niet annuleert. Elke toggle plaatst een aparte worker-aanvraag in de wachtrij, en wanneer de worker 2+ seconden per cyclus bezig is, groeit de wachtrij onbegrensd.

De juiste aanpak is een synchronisatie-referentie:


// Synchroon ingesteld in de click-handler wanneer de worker bezig is

pendingToggleRef.current = true;

// Gecontroleerd in de onmessage-callback van de worker — stuurt alleen de eindtoestand

if (pendingToggleRef.current) {
  pendingToggleRef.current = false;
  worker.postMessage({ ...latestState });

}

Er wordt geen wachtrij gecreëerd. De worker ontvangt maximaal één aanvraag per cyclus, met de uiteindelijke toggle-toestand van de gebruiker.

Gecontroleerde Spam: 2 Minuten Aanhoudend Misbruik

Het ingedrukt houden van Enter op de Sort Keys-toggle gedurende 2 minuten achter elkaar leverde 22 schone cycli op:

MetriekWaarde
Totale worker-reacties22
Duur~120 s
Gemiddelde cyclustijd~5,5 s
Pieker JS Heap270 MB
JS Heap (na idle GC)183 MB

Elke cyclus bundelde honderden toetsaanslagen in één enkele aanvraag. De tab bevroor nooit en overschreed nooit 270 MB JS-heap.

Alle 22 reacties toonden parseTimeMs=0, wat bevestigt dat elke aanvraag na de eerste het gecachte object van de worker trof. De 2-4 seconden cyclustijd werd gedomineerd door de deep clone en sorteerbewerking, niet door JSON-parsing.

Extreem Misbruik: Ononderbroken Toggle-klikken

Een agressiever scenario — snel schakelen tussen meerdere toggle-knoppen (sort keys, minify, indent) gedurende 2,5 minuten — duwt de architectuur in een andere foutmodus. De coalescentie-referentie voorkomt wachtrijopbouw, maar legt geen rustinterval op. Zolang de gebruiker blijft klikken, activeert elke voltooide cyclus onmiddellijk de volgende, waardoor de worker op 100% inschakelduur blijft zonder inactiviteitsvenster voor garbage collection.

Dit produceerde meer dan 50 opeenvolgende cycli met gestaag stijgend geheugen:

MetriekWaarde
Totale worker-reacties50+
Duur~150 s
Pieker JS Heap3,2 GB

Zelfs bij piekgeheugen bleven alle UI-besturingselementen visueel responsief. Dropdowns openden en sloten. Toggle-schakelaars animeerden tussen aangevinkte en niet-aangevinkte toestanden. De gebruiker kon met elke knop of elk menu interageren.

Twee dingen braken echter:

Statusbalk bleef hangen op “Processing”. Nadat de gebruiker stopte met klikken en het geheugen stabiliseerde op 1,0-1,1 GB, bleef de statusbalk “Worker: Processing…” tonen zonder herstel. Onder 3,2 GB heap-druk gooide React’s scheduler de in startTransition gewikkelde toestandsupdate van de laatste worker-reactie weg, waardoor isProcessing permanent true bleef. De 300-ms-afkoeltimer werd waarschijnlijk ook weggegooid.

Het uitvoerpaneel stopte met bijwerken. De geformatteerde JSON in de uitvoereditor weerspiegelde nooit de uiteindelijke toggle-toestand. De worker had zijn reactie voltooid en verzonden (zichtbaar in consolelogs), maar setOutputCode() was samen met setIsProcessing(false) in startTransition gewikkeld, en beide werden weggegooid.

Dit is een bewust randgeval, geen bug. Als een gebruiker Enter op een toggle 2,5 minuten ingedrukt houdt en zijn browser naar 3,2 GB heap duwt, werkt de applicatie buiten haar ontwerpparameters. Elke technische tegenmaatregel (watchdog-timer, gedwongen afkoeling) zou de 99,9% van normale interacties bestraffen, alleen om zich te verdedigen tegen een scenario dat een paginavers verversing in 2 seconden oplost. U kunt geen web-app ontwerpen die kwaadwillig misbruik overleeft zonder de ervaring voor iedereen te verslechteren.

Het verschil tussen 270 MB en 3,2 GB is geen coalescentiefout. Beide scenario’s bundelen klikken in enkele aanvragen per cyclus. Het 3,2 GB-scenario voert simpelweg 50+ cycli back-to-back uit zonder adempauze, waardoor er geen venster overblijft voor GC of React’s scheduler. Dit valt buiten de ontwerpparameters van de applicatie.

Idle GC en Cache-miss Herstel

De worker gebruikt een 60-seconden inactiviteitstimer die zijn interne gecachte object vrijgeeft om langdurig geheugengebruik te voorkomen. Na 60 seconden inactiviteit, activeert het klikken van een toggle een cache-miss en stuurt de hoofdthread de volledige invoerstring opnieuw:

FaseTijd
Cache-miss detectie~0 ms
Code-herverzending + koude parse1.486 ms
Stringify108 ms
Totale herstel3.795 ms

Na voltooiing van de koude herstel stabiliseerde de heap zich op 183 MB, lager dan de initiële 232 MB, wat aangeeft dat de garbage collector de tussentijdse toewijzingen van de eerdere toggle-spam had teruggewonnen.

Volledig Geheugenprofiel

SnapshotConditieJS Heap
1Na initiële formatting232 MB
2Na 2 min toggle-spam270 MB
3Na 60 s idle GC + herstel183 MB
4Na wissen34 MB

De applicatie herstelt naar bijna basisgeheugen na het wissen van de invoer, wat bevestigt dat er geen lekkages zijn in de worker-levenscyclus of het editor-toestandsbeheer.

Belangrijkste Inzichten

  1. Web Worker-delegatie elimineert UI-bevriezing: zelfs bij 102 MB blijft de hoofdthread responsief gedurende alle verwerking.

  2. Toggle-coalescentie voorkomt wachtrijopbouw: pendingToggleRef bundelt N klikken in 1 aanvraag. Een tijdgebaseerde debounce kan dit niet garanderen omdat vertragingen niet annuleren.

  3. Piekgeheugen hangt af van misbruikduur: bij aanhoudende 2-minuten-spam met natuurlijke pauzes piekt de heap op 270 MB. Bij ononderbroken klikken zonder rustintervallen (50+ opeenvolgende cycli) kan de heap 3,2 GB bereiken. Een afkoeltimer tussen cycli zou deze kloof dichten.

  4. Cache-miss herstel is geleidelijk: de worker signaleert cache-ongeldigheid, de hoofdthread stuurt de invoer opnieuw en de verwerking wordt voltooid zonder gegevensverlies.

Het technische patroon dat dit mogelijk maakt, is eenvoudig in principe maar vereist discipline bij handhaving: blokkeer de hoofdthread met niets, coalesceer elke invoervariantie in exact één lopende toestand en geef elke toewijzer een pad terug naar nul.

JSONClear. Crafted without compromise by Aerisium.