Teste de Estresse JSON de 100 MB: Throughput de Workers, Tetos de Memória e Coalescência de Alternâncias

Por Aerisium Core · ·

Um arquivo JSON de 100 MB não deve crashar uma aba do navegador. A abordagem ingênua garante falha nesta escala: parsear na thread principal, fazer debounce de cada alternância, manter o DOM completo na memória. Mas substituir cada um desses padrões por uma alternativa correta (Web Worker, referências de coalescência, virtualização do DOM) produz uma arquitetura que sobrevive à pior entrada sem congelamento visível.

Este artigo documenta medições reais da carga e manipulação de um payload JSON de 102 MB em um editor JSON baseado em navegador.

Ambiente de Teste

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 chaves

O payload consistia em 180.000 objetos, cada um com um campo id e uma string data de 400 caracteres, envolvidos em um array. Esta estrutura imita um dump de logs grande ou uma exportação de banco de dados.

Formatação Inicial: Throughput do Worker

O arquivo foi carregado via arrastar e soltar. FileReader.readAsText() produziu uma string raw, que foi enviada a um Web Worker para parse e formatação. O handler do worker foi instrumentado com performance.now() para isolar o tempo de parse do tempo 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;
FaseTempo
Round-trip total do worker2.463 ms
JSON.parse()581 ms
JSON.stringify() com indentação409 ms

Os ~1,5 segundos restantes foram consumidos pela contagem de chaves (percurso recursivo de todas as 1,6M chaves) e uma operação de ordenação por clonagem profunda (sortObjKeys) quando a ordenação de chaves estava ativa.

O total de 2,5 segundos está bem abaixo do limiar de “Página não responde” do navegador. A thread de UI permaneceu completamente responsiva porque todo o processamento pesado foi executado na thread de fundo do worker.

Alocação de Memória Após o Parse Inicial

Uma snapshot do heap tirada imediatamente após o worker retornar o resultado:

MediçãoValor
JS Heap (formatação inicial)232 MB
Memória total da aba647 MB

Uma string de 102 MB se expande para aproximadamente 4 vezes seu tamanho de arquivo em memória devido à codificação UTF-16 do JavaScript. A sobrecarga restante inclui a árvore de estado do editor, a string de saída formatada e a representação interna de objetos do V8 da estrutura analisada. O V8 aloca memória para Hidden Classes, backing stores de propriedades e backing stores de elementos para cada objeto no array. Cada um dos 180.000 objetos carrega uma sobrecarga estrutural fixa além dos bytes de chave e valor.

Coalescência de Alternâncias: Referência de Sincronização

A abordagem ingênua baseada em tempos (um debounce de 50 ms em cauda) falha porque apenas atrasa —não cancela. Cada alternância enfileira uma solicitação de worker separada, e quando o worker está ocupado por 2+ segundos por ciclo, a fila cresce sem limites.

A abordagem correta é uma referência de sincronização:


// Definido sincronamente no handler de clique quando o worker está ocupado

pendingToggleRef.current = true;

// Verificado no callback onmessage do worker — envia apenas o estado final

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

}

Nenhuma fila é criada. O worker recebe no máximo uma solicitação por ciclo, carregando o estado final de alternância do usuário.

Spam Controlado: 2 Minutos de Abuso Sustentado

Manter Enter pressionado na alternância Sort Keys por 2 minutos seguidos produziu 22 ciclos limpos:

MétricaValor
Respostas totais do worker22
Duração~120 s
Tempo de ciclo médio~5,5 s
JS Heap máximo270 MB
JS Heap (após GC inativo)183 MB

Cada ciclo colapsou centenas de pressionamentos de tecla em uma única solicitação. A aba nunca congelou e nunca excedeu 270 MB de JS heap.

Todas as 22 respostas mostraram parseTimeMs=0, confirmando que cada solicitação após a primeira encontrou o objeto em cache do worker. O tempo de ciclo de 2-4 segundos era dominado pela clonagem profunda e ordenação, não pelo parse JSON.

Abuso Extremo: Cliques de Alternância Ininterruptos

Um cenário mais agressivo —alternando entre múltiplos botões de alternância (sort keys, minify, indent) o mais rápido possível por 2,5 minutos— leva a arquitetura a um modo de falha diferente. A referência de coalescência evita o acúmulo de fila, mas não impõe um intervalo de descanso. Enquanto o usuário continuar clicando, cada ciclo completado desencadeia imediatamente o seguinte, mantendo o worker em 100% de ciclo de trabalho sem janela de inatividade para coleta de lixo.

Isso produziu mais de 50 ciclos consecutivos com a memória aumentando de forma constante:

MétricaValor
Respostas totais do worker50+
Duração~150 s
JS Heap máximo3,2 GB

Mesmo com a memória no máximo, todos os controles de UI permaneciam visualmente responsivos. Os dropdowns abriam e fechavam. Os interruptores de alternância animavam entre estados marcados e não marcados. O usuário podia interagir com qualquer botão ou menu.

No entanto, duas coisas quebraram:

Barra de status travada em “Processing”. Após o usuário parar de clicar e a memória se estabilizar em 1.0-1.1 GB, a barra de status continuava mostrando “Worker: Processing…” sem recuperação. Sob pressão de 3,2 GB de heap, o scheduler do React descartou a atualização de estado envolvida em startTransition da última resposta do worker, deixando isProcessing permanentemente true. O temporizador de resfriamento de 300 ms também foi provavelmente descartado.

O painel de saída parou de atualizar. O JSON formatado no editor de saída nunca refletiu o estado final de alternância. O worker havia completado e enviado sua resposta (visível nos logs do console), mas setOutputCode() estava envolto em startTransition junto com setIsProcessing(false), e ambos foram descartados.

Este é um caso limite deliberado, não um bug. Se um usuário mantém Enter em uma alternância por 2,5 minutos seguidos e leva seu navegador a 3,2 GB de heap, a aplicação opera fora de seus parâmetros de projeto. Qualquer contramedida de engenharia (temporizador watchdog, resfriamento forçado) penalizaria os 99,9% das interações normais apenas para se defender de um cenário que um recarregamento de página resolve em 2 segundos. Não é possível projetar uma aplicação web para sobreviver a abuso malicioso sem degradar a experiência para todos os outros.

A diferença entre 270 MB e 3,2 GB não é uma falha de coalescência. Ambos os cenários colapsam cliques em solicitações únicas por ciclo. O cenário de 3,2 GB simplesmente executa 50+ ciclos consecutivos sem margem de respiro, não deixando janela para GC ou o scheduler do React. Isto está fora dos parâmetros de projeto da aplicação.

GC Inativo e Recuperação de Falha de Cache

O worker emprega um temporizador de inatividade de 60 segundos que libera seu objeto interno em cache para evitar retenção de memória a longo prazo. Após 60 segundos de inatividade, clicar em uma alternância desencadeia uma falha de cache e a thread principal reenvia a string de entrada completa:

FaseTempo
Detecção de falha de cache~0 ms
Reenvio de código + parse a frio1.486 ms
Stringify108 ms
Recuperação total3.795 ms

Após completar a recuperação a frio, o heap se estabilizou em 183 MB, menos que os 232 MB iniciais, indicando que o coletor de lixo havia recuperado as alocações intermediárias do spam de alternâncias anterior.

Perfil de Memória Completo

SnapshotCondiçãoJS Heap
1Após formatação inicial232 MB
2Após 2 min de spam de alternâncias270 MB
3Após 60 s de GC inativo + recuperação183 MB
4Após limpar34 MB

A aplicação se recupera para uma memória quase basal após limpar a entrada, confirmando que não há vazamentos no ciclo de vida do worker nem na gestão de estado do editor.

Conclusões Principais

  1. A delegação a Web Workers elimina congelamentos de UI: mesmo a 102 MB, a thread principal permanece responsiva durante todo o processamento.

  2. A coalescência de alternâncias evita acúmulo de fila: pendingToggleRef colapsa N cliques em 1 solicitação. Um debounce baseado em tempos não pode garantir isso porque atrasos não cancelam.

  3. A memória máxima depende da duração do abuso: sob spam sustentado de 2 minutos com pausas naturais, o heap atinge um máximo de 270 MB. Sob cliques ininterruptos sem intervalos de descanso (50+ ciclos consecutivos), o heap pode atingir 3,2 GB. Um temporizador de resfriamento entre ciclos eliminaria esta lacuna.

  4. A recuperação de falha de cache é gradual: o worker sinaliza invalidação de cache, a thread principal reenvia a entrada e o processamento é concluído sem perda de dados.

O padrão de engenharia que permite isto é simples em princípio mas requer disciplina na aplicação: não bloquear a thread principal com nada, coalescer cada variação de entrada exatamente em um estado pendente, e dar a cada alocador um caminho de volta a zero.

JSONClear. Crafted without compromise by Aerisium.