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
| Parameter | Waarde |
|---|---|
| CPU | Intel Core i5-8250U @ 1,60 GHz (4 cores, 8 logisch) |
| RAM | 8 GB |
| OS | Windows 10 22H2 |
| Browser | Chrome 149.0.7827.201 |
| Payload | 102,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;
| Fase | Tijd |
|---|---|
| Totale worker-roundtrip | 2.463 ms |
JSON.parse() | 581 ms |
JSON.stringify() met inspringing | 409 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:
| Meting | Waarde |
|---|---|
| JS Heap (initiële formatting) | 232 MB |
| Totaal tab-geheugen | 647 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:
| Metriek | Waarde |
|---|---|
| Totale worker-reacties | 22 |
| Duur | ~120 s |
| Gemiddelde cyclustijd | ~5,5 s |
| Pieker JS Heap | 270 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:
| Metriek | Waarde |
|---|---|
| Totale worker-reacties | 50+ |
| Duur | ~150 s |
| Pieker JS Heap | 3,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:
| Fase | Tijd |
|---|---|
| Cache-miss detectie | ~0 ms |
| Code-herverzending + koude parse | 1.486 ms |
| Stringify | 108 ms |
| Totale herstel | 3.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
| Snapshot | Conditie | JS Heap |
|---|---|---|
| 1 | Na initiële formatting | 232 MB |
| 2 | Na 2 min toggle-spam | 270 MB |
| 3 | Na 60 s idle GC + herstel | 183 MB |
| 4 | Na wissen | 34 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
-
Web Worker-delegatie elimineert UI-bevriezing: zelfs bij 102 MB blijft de hoofdthread responsief gedurende alle verwerking.
-
Toggle-coalescentie voorkomt wachtrijopbouw:
pendingToggleRefbundelt N klikken in 1 aanvraag. Een tijdgebaseerde debounce kan dit niet garanderen omdat vertragingen niet annuleren. -
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.
-
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.