Pruebas de Estrés JSON de 100 MB: Rendimiento de Workers, Límites de Memoria y Coalescencia de Alternancias

Por Aerisium Core · ·

Un archivo JSON de 100 MB no debería colapsar una pestaña del navegador. El enfoque ingenuo garantiza el fallo a esta escala: analizar en el hilo principal, debouncear cada alternancia, mantener el DOM completo en memoria. Pero reemplazar cada uno de esos patrones con una alternativa correcta (Web Worker, referencias de coalescencia, virtualización del DOM) produce una arquitectura que sobrevive a la entrada peor sin congelamiento visible.

Este artículo documenta mediciones reales de la carga y manipulación de un payload JSON de 102 MB en un editor JSON basado en navegador.

Entorno de Prueba

ParámetroValor
CPUIntel Core i5-8250U @ 1.60GHz (4 núcleos, 8 lógicos)
RAM8 GB
SOWindows 10 22H2
NavegadorChrome 149.0.7827.201
Payload102.67 MB JSON, 1.601.887 claves

El payload consistía en 180.000 objetos, cada uno con un campo id y una cadena data de 400 caracteres, envueltos en un array. Esta estructura imita un volcado de logs grande o una exportación de base de datos.

Formateo Inicial: Rendimiento del Worker

El archivo se cargó mediante arrastrar y soltar. FileReader.readAsText() produjo una cadena raw, que se envió a un Web Worker para análisis y formateo. El manejador del worker se instrumentó con performance.now() para aislar el tiempo de parseo del tiempo de stringify:


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;
FaseTiempo
Round-trip total del worker2.463 ms
JSON.parse()581 ms
JSON.stringify() con indentación409 ms

Los ~1,5 segundos restantes fueron consumidos por el conteo de claves (recorrido recursivo de todas las 1,6M claves) y una operación de ordenación por clonación profunda (sortObjKeys) cuando la ordenación de claves estaba activa.

El total de 2,5 segundos está muy por debajo del umbral de “Página no responde” del navegador. El hilo de UI permaneció completamente responsivo porque todo el cómputo pesado se ejecutó en el hilo de fondo del worker.

Asignación de Memoria Tras el Parseo Inicial

Una instantánea del heap tomada inmediatamente después de que el worker devolvió el resultado:

MediciónValor
JS Heap (formateo inicial)232 MB
Memoria total de la pestaña647 MB

Una cadena de 102 MB se expande a aproximadamente 4 veces su tamaño de archivo en memoria debido a la codificación UTF-16 de JavaScript. La sobrecarga restante incluye el árbol de estado del editor, la cadena de salida formateada y la representación interna de objetos de V8 de la estructura analizada. V8 asigna memoria para Hidden Classes, backing stores de propiedades y backing stores de elementos para cada objeto en el array. Cada uno de los 180.000 objetos carga una sobrecarga estructural fija más allá de los bytes de clave y valor.

Coalescencia de Alternancias: Referencia de Sincronización

El enfoque ingenuo basado en tiempos (un debounce de 50 ms en cola) falla porque solo retrasa —no cancela. Cada alternancia encola una solicitud de worker separada, y cuando el worker está ocupado durante 2+ segundos por ciclo, la cola crece sin límite.

El enfoque correcto es una referencia de sincronización:


// Se establece sincrónicamente en el manejador de clic cuando el worker está ocupado

pendingToggleRef.current = true;

// Se verifica en el callback onmessage del worker — envía solo el estado final

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

}

No se crea ninguna cola. El worker recibe como máximo una solicitud por ciclo, que lleva el estado final de alternancia del usuario.

Spam Controlado: 2 Minutos de Abuso Sostenido

Mantener Enter presionado en la alternancia Sort Keys durante 2 minutos seguidos produjo 22 ciclos limpios:

MétricaValor
Respuestas totales del worker22
Duración~120 s
Tiempo de ciclo medio~5,5 s
JS Heap máximo270 MB
JS Heap (tras GC inactivo)183 MB

Cada ciclo colapsó cientos de pulsaciones de teclas en una sola solicitud. La pestaña nunca se congeló y nunca superó los 270 MB de JS heap.

Las 22 respuestas mostraron parseTimeMs=0, confirmando que cada solicitud tras la primera encontró el objeto en caché del worker. El tiempo de ciclo de 2-4 segundos estaba dominado por la clonación profunda y la ordenación, no por el parseo JSON.

Abuso Extremo: Clics de Alternancia Ininterrumpidos

Un escenario más agresivo —alternando entre múltiples botones de alternancia (sort keys, minify, indent) lo más rápido posible durante 2,5 minutos— empuja la arquitectura a un modo de fallo diferente. La referencia de coalescencia evita la acumulación de cola, pero no impone un intervalo de descanso. Mientras el usuario siga haciendo clic, cada ciclo completado desencadena inmediatamente el siguiente, manteniendo el worker al 100 % de ciclo de trabajo sin ventana de inactividad para la recolección de basura.

Esto produjo más de 50 ciclos consecutivos con la memoria aumentando de forma constante:

MétricaValor
Respuestas totales del worker50+
Duración~150 s
JS Heap máximo3,2 GB

Incluso con la memoria al máximo, todos los controles de UI seguían siendo visualmente responsivos. Los desplegables se abrían y cerraban. Los interruptores de alternancia se animaban entre estados marcados y no marcados. El usuario podía interactuar con cualquier botón o menú.

Sin embargo, dos cosas se rompieron:

Barra de estado atascada en “Processing”. Después de que el usuario dejó de hacer clic y la memoria se estabilizó en 1.0-1.1 GB, la barra de estado seguía mostrando “Worker: Processing…” sin recuperación. Bajo presión de 3,2 GB de heap, el scheduler de React eliminó la actualización de estado envuelta en startTransition de la última respuesta del worker, dejando isProcessing permanentemente en true. El temporizador de enfriamiento de 300 ms también fue probablemente eliminado.

El panel de salida dejó de actualizarse. El JSON formateado en el editor de salida nunca reflejó el estado final de alternancia. El worker había completado y enviado su respuesta (visible en los logs de consola), pero setOutputCode() estaba envuelto en startTransition junto con setIsProcessing(false), y ambos fueron eliminados.

Este es un caso límite deliberado, no un error. Si un usuario mantiene Enter en una alternancia durante 2,5 minutos seguidos y empuja su navegador a 3,2 GB de heap, la aplicación opera fuera de sus parámetros de diseño. Cualquier contramedida de ingeniería (temporizador watchdog, enfriamiento forzado) penalizaría el 99,9 % de las interacciones normales solo para defenderse de un escenario que una recarga de página resuelve en 2 segundos. No se puede diseñar una aplicación web para sobrevivir al abuso malicioso sin degradar la experiencia para todos los demás.

La diferencia entre 270 MB y 3,2 GB no es un fallo de coalescencia. Ambos escenarios colapsan los clics en solicitudes únicas por ciclo. El escenario de 3,2 GB simplemente ejecuta 50+ ciclos consecutivos sin margen de respiro, sin dejar ventana para GC o el scheduler de React. Esto está fuera de los parámetros de diseño de la aplicación.

GC Inactivo y Recuperación de Fallo de Caché

El worker emplea un temporizador de inactividad de 60 segundos que libera su objeto interno en caché para evitar la retención de memoria a largo plazo. Después de 60 segundos de inactividad, al hacer clic en una alternancia se desencadena un fallo de caché y el hilo principal reenvía la cadena de entrada completa:

FaseTiempo
Detección de fallo de caché~0 ms
Reenvío de código + parseo en frío1.486 ms
Stringify108 ms
Recuperación total3.795 ms

Después de completar la recuperación en frío, el heap se estabilizó en 183 MB, menos que los 232 MB iniciales, lo que indica que el recolector de basura había reclamado las asignaciones intermedias del spam de alternancias anterior.

Perfil de Memoria Completo

InstantáneaCondiciónJS Heap
1Tras formateo inicial232 MB
2Tras 2 min de spam de alternancias270 MB
3Tras 60 s de GC inactivo + recuperación183 MB
4Tras limpiar34 MB

La aplicación se recupera a una memoria casi basal después de limpiar la entrada, confirmando que no hay fugas en el ciclo de vida del worker ni en la gestión del estado del editor.

Conclusiones Clave

  1. La delegación a Web Workers elimina los congelamientos de UI: incluso a 102 MB, el hilo principal permanece responsivo durante todo el procesamiento.

  2. La coalescencia de alternancias evita la acumulación de cola: pendingToggleRef colapsa N clics en 1 solicitud. Un debounce basado en tiempos no puede garantizar esto porque los retrasos no cancelan.

  3. La memoria máxima depende de la duración del abuso: bajo spam sostenido de 2 minutos con pausas naturales, el heap alcanza un máximo de 270 MB. Bajo clics ininterrumpidos sin intervalos de descanso (50+ ciclos consecutivos), el heap puede alcanzar 3,2 GB. Un temporizador de enfriamiento entre ciclos eliminaría esta brecha.

  4. La recuperación de fallo de caché es gradual: el worker señala la invalidación de caché, el hilo principal reenvía la entrada y el procesamiento se completa sin pérdida de datos.

El patrón de ingeniería que permite esto es sencillo en principio pero requiere disciplina en su aplicación: no bloquear el hilo principal con nada, coalescer cada variación de entrada exactamente en un estado pendiente, y dar a cada asignador un camino de vuelta a cero.

JSONClear. Crafted without compromise by Aerisium.