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âmetro | Valor |
|---|---|
| CPU | Intel Core i5-8250U @ 1.60GHz (4 núcleos, 8 lógicos) |
| RAM | 8 GB |
| SO | Windows 10 22H2 |
| Navegador | Chrome 149.0.7827.201 |
| Payload | 102,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;
| Fase | Tempo |
|---|---|
| Round-trip total do worker | 2.463 ms |
JSON.parse() | 581 ms |
JSON.stringify() com indentação | 409 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ção | Valor |
|---|---|
| JS Heap (formatação inicial) | 232 MB |
| Memória total da aba | 647 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étrica | Valor |
|---|---|
| Respostas totais do worker | 22 |
| Duração | ~120 s |
| Tempo de ciclo médio | ~5,5 s |
| JS Heap máximo | 270 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étrica | Valor |
|---|---|
| Respostas totais do worker | 50+ |
| Duração | ~150 s |
| JS Heap máximo | 3,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:
| Fase | Tempo |
|---|---|
| Detecção de falha de cache | ~0 ms |
| Reenvio de código + parse a frio | 1.486 ms |
| Stringify | 108 ms |
| Recuperação total | 3.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
| Snapshot | Condição | JS Heap |
|---|---|---|
| 1 | Após formatação inicial | 232 MB |
| 2 | Após 2 min de spam de alternâncias | 270 MB |
| 3 | Após 60 s de GC inativo + recuperação | 183 MB |
| 4 | Após limpar | 34 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
-
A delegação a Web Workers elimina congelamentos de UI: mesmo a 102 MB, a thread principal permanece responsiva durante todo o processamento.
-
A coalescência de alternâncias evita acúmulo de fila:
pendingToggleRefcolapsa N cliques em 1 solicitação. Um debounce baseado em tempos não pode garantir isso porque atrasos não cancelam. -
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.
-
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.