JSON Toolkit

Análise, renderização e streaming de JSON grande

Resumo: A análise muitas vezes não é o gargalo; a renderização e a cópia são. Otimize ao longo das quatro camadas "analisar → percorrer → renderizar → transportar" para não desperdiçar esforço.

Intermediário~13 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-31
Capa de Análise, renderização e streaming de JSON grande

Resumo da ópera: meça análise, travessia, renderização e transporte separadamente primeiro, depois trabalhe nesta ordem — corte cópias, expanda sob demanda com rolagem virtual, mova o trabalho para um Worker, mude para streaming e só então mude o formato. Streaming só compensa quando o arquivo não cabe na memória ou quando você precisa de um pequeno subconjunto dos seus campos.

A conclusão "JSON.parse é lento" está errada oito em cada dez vezes. O que é realmente lento é o que você faz depois de analisar.

1. Meça primeiro, depois otimize

Para o mesmo arquivo JSON de 10MB, o tempo gasto em etapas diferentes difere por ordens de magnitude:

Etapa Duração típica Custo principal
JSON.parse (nativo) Dezenas a pouco mais de cem milissegundos Uma única varredura linear mais construção de objetos
Cópia profunda (structuredClone / recursiva) Possivelmente 1–3 vezes a análise Reconstruir o grafo de objetos inteiro
Comparação profunda / diff Possivelmente o dobro de novo Travessia recursiva mais comparação de strings
Renderização em uma árvore DOM Congela quando há dezenas de milhares de nós Layout e reflow
Concatenação de strings e depois serialização Proporcional ao tamanho da saída Dobra a memória

Então meça antes de agir: no navegador, envolva cada etapa com performance.now(), ou use console.time; no Node, use process.memoryUsage() para observar o crescimento do heap. Sem linha de base, sem otimização.

2. Camada de análise: três estratégias

2.1 Análise completa (a escolha padrão)

const data = JSON.parse(text);

A implementação nativa faz a varredura e a construção de objetos em C++, então uma biblioteca puramente em JS dificilmente a supera. Use-a enquanto as três condições a seguir valerem:

  • O texto cabe inteiramente na memória (normalmente < 100MB);
  • A maioria dos campos é realmente necessária após a análise;
  • O grau de desconfiança dos dados é administrável.

2.2 Análise incremental em streaming

Com um analisador no estilo SAX como stream-json, você consegue varrer os campos alvo sem construir uma árvore de objetos completa:

import { createReadStream } from 'node:fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';

const pipeline = createReadStream('huge.json')
  .pipe(parser())
  .pipe(pick({ filter: 'items' }))
  .pipe(streamArray());

for await (const { value } of pipeline) {
  // Processa um elemento por vez; a memória independe da contagem de elementos
  handleItem(value);
}

Custos:

  • A complexidade do código aumenta significativamente e o tratamento de erros fica mais confuso;
  • Só é possível acesso sequencial, sem acesso aleatório;
  • É pouco amigável a requisitos que precisam de agregação global.

Regra de decisão: streaming só compensa quando o arquivo é grande demais para caber, ou quando você precisa apenas de um subconjunto muito pequeno dos seus campos.

2.3 Mude o formato

Se os dados são "um lote de registros" em vez de "um único documento", você deveria ter usado JSON Lines desde o início. Um formato delimitado por linha transforma "streaming" em "ler linha a linha", reduzindo a complexidade de implementação em uma ordem de magnitude.

3. Camada de travessia: uma cópia a menos ganha metade da batalha

// Antipadrão: copiar profundamente primeiro "por segurança"
const safeCopy = structuredClone(bigData);   // A memória dobra imediatamente

// Bom padrão: passe referências diretamente em cenários somente leitura
function renderSummary(data: OrderDocument): Summary {
  return { total: data.items.length, amount: data.total };
}

Cópias ocultas comuns:

  • JSON.parse(JSON.stringify(x)) usado como "cópia profunda" — este é o tipo mais caro;
  • { ...data } copia apenas um nível, mas para um objeto grande ainda copia o array de referências de todas as chaves de nível superior;
  • Array.prototype.map cria um novo array, o que também é caro quando há muitos elementos;
  • Proxies de reatividade de frameworks (o reactive do Vue) fazem proxy de objetos grandes recursivamente, e o overhead pode ser maior que a própria análise.

Conselho prático: mantenha os "dados crus" como um objeto simples imutável e não os envolva em reatividade; calcule e faça cache separadamente dos dados derivados que você precisa exibir.

4. Camada de renderização: não enfie a árvore inteira no DOM

Um arquivo JSON de 5MB tem aproximadamente 30.000 a 100.000 nós; renderizá-los todos como elementos DOM fará o navegador parar de responder.

Três abordagens viáveis:

4.1 Expandir sob demanda

Renderize apenas os filhos diretos do nó raiz e calcule o próximo nível apenas quando o nó for expandido. Esta é a abordagem padrão para visualizadores de árvore:

type TreeNode = { key: string; value: unknown; expanded: boolean };

/**
 * Retorna apenas os nós visíveis no momento, evitando construir a árvore inteira de uma vez.
 * @param rootNode Nó raiz
 * @param expandedPaths Conjunto de caminhos que foram expandidos
 * @returns Lista de nós visíveis
 */
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
  const visibleNodes: TreeNode[] = [];

  /**
   * Coleta recursivamente os nós filhos dos ramos expandidos.
   * @param currentNode Nó atual
   * @param currentPath Caminho atual
   */
  const walk = (currentNode: TreeNode, currentPath: string): void => {
    visibleNodes.push(currentNode);
    if (!expandedPaths.has(currentPath) || typeof currentNode.value !== 'object' || currentNode.value === null) {
      return;
    }
    for (const [childKey, childValue] of Object.entries(currentNode.value)) {
      walk({ key: childKey, value: childValue, expanded: false }, `${currentPath}/${childKey}`);
    }
  };

  walk(rootNode, '');
  return visibleNodes;
}

4.2 Rolagem virtual

Quando os nós expandidos passam de algumas centenas, renderize apenas as poucas dezenas dentro do viewport e reserve espaço para o resto com alturas de placeholder. É mais eficaz em visões no estilo de lista (tabelas, logs).

4.3 Mostre apenas resumos

Apresente primeiro "estrutura + tipo + contagem" (por exemplo items: Array(12000)); só quando o usuário abre um item você busca o valor concreto. Esse truque funciona melhor para o cenário "só quero confirmar se os campos estão certos".

5. Thread principal: mova o trabalho para um Worker quando for o caso

Analisar, formatar e comparar qualquer coisa maior que 1MB deve ir para um Web Worker:

// json.worker.ts
self.onmessage = (event: MessageEvent<{ text: string }>) => {
  try {
    const parsed = JSON.parse(event.data.text);
    self.postMessage({ ok: true, data: parsed });
  } catch (parseError) {
    self.postMessage({ ok: false, message: (parseError as Error).message });
  }
};
/**
 * Analisa JSON dentro de um Worker para que a thread principal não seja bloqueada.
 * @param text Texto JSON
 * @returns Resultado da análise
 */
function parseInWorker(text: string): Promise<unknown> {
  return new Promise((resolvePromise, rejectPromise) => {
    const worker = new Worker(new URL('./json.worker.ts', import.meta.url), { type: 'module' });
    worker.onmessage = (event) => {
      worker.terminate();
      if (event.data.ok) {
        resolvePromise(event.data.data);
      } else {
        rejectPromise(new Error(event.data.message));
      }
    };
    worker.postMessage({ text });
  });
}

Cuidado com o custo de comunicação dos Workers: postMessage usa clonagem estruturada por padrão, e clonar um objeto de 10MB uma vez também leva dezenas de milissegundos. Se tudo o que você precisa é "analisar e entregar à thread principal", faça o trabalho pesado dentro do Worker (analisar + transformar + validar) e envie de volta um resumo do resultado.

6. Camada de transporte: otimização de tamanho

Técnica Ganho típico Custo
gzip / brotli A saída comprimida costuma ser 10%–25% do tamanho original Requer suporte do servidor
Remover indentação e espaços em branco Redução de 20%–40% Não é legível por humanos
Abreviar nomes de campo Ganhos perceptíveis com aninhamento profundo Menor legibilidade; requer tabela de mapeamento
Números em vez de strings de enum Pequeno Requer um dicionário
Paginação / campos sob demanda O maior Redesenho da API

Para páginas voltadas a humanos, a combinação correta é: a API envia JSON compacto e comprimido, e a interface o formata apenas para exibição. Muitos sites de ferramentas por padrão enviam texto formatado de volta ao frontend, enviando 30% mais bytes à toa.

7. Como fazer benchmark

/**
 * Mede a duração mediana de uma rotina síncrona para que uma única execução instável não distorça o resultado.
 * @param runOnce Função sob teste
 * @param repeatCount Número de repetições
 * @returns Duração mediana em milissegundos
 */
function measureMedian(runOnce: () => void, repeatCount = 9): number {
  const durations: number[] = [];
  for (let roundIndex = 0; roundIndex < repeatCount; roundIndex += 1) {
    const startedAt = performance.now();
    runOnce();
    durations.push(performance.now() - startedAt);
  }
  durations.sort((firstValue, secondValue) => firstValue - secondValue);
  return durations[Math.floor(durations.length / 2)];
}

Três regras de disciplina:

  1. Use uma amostra fixa e nunca gere dados aleatoriamente, ou os números não serão comparáveis;
  2. Rode pelo menos 9 vezes e pegue a mediana; a primeira execução inclui aquecimento do JIT e precisa ser descartada;
  3. Registre o ambiente de execução (versão do Node, CPU), ou comparações entre máquinas não fazem sentido.

8. Lista de verificação

  • [ ] Existem números de linha de base para que antes e depois possam ser comparados;
  • [ ] A análise de qualquer coisa maior que 1MB roda em um Worker;
  • [ ] As visões em árvore expandem sob demanda e as listas usam rolagem virtual;
  • [ ] Nenhuma cópia profunda desnecessária nem envolvimento em reatividade;
  • [ ] JSON Lines é usado quando os dados são "um lote de registros";
  • [ ] A API envia JSON compacto e a formatação acontece apenas para exibição.