JSON Toolkit

FAQ de desempenho e segurança: lento, travado e dados não confiáveis

Resumo: Por que um arquivo JSON de dezenas de MB não abre, quanto custa uma clonagem profunda, como prevenir prototype pollution e por onde entra o XSS — dez conclusões de primeiros socorros organizadas por sintoma.

Intermediário~13 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa da FAQ de desempenho e segurança

Resumo da ópera: diagnostique pelo sintoma primeiro — análise, renderização ou memória — depois siga a ordem de correção: corte o volume de dados, pare de clonar profundamente, mova a análise para um Worker com rolagem virtual e faça streaming de qualquer coisa acima de 50 MB. Para entrada não confiável, limite tamanho e profundidade de aninhamento antes de filtrar __proto__, constructor e prototype.

"JSON é lento" está errado nove em cada dez vezes; o que é realmente lento costuma ser a renderização, a clonagem profunda ou a ordem errada de operações.

1. Por que está lento

Analisar um arquivo JSON grande é lento. Como otimizo?

Localize o gargalo primeiro, fale de otimização depois. As etapas variam por várias ordens de magnitude:

Etapa Tempo aproximado para 10 MB de dados Por que é o gargalo
Transferência de rede Depende da largura de banda JSON sem compressão normalmente comprime para cerca de 1/5
JSON.parse 20–80 ms Uma implementação nativa, já rápida
Travessia recursiva / clonagem profunda 200–2000 ms Processamento puro em JS, nó por nó
Renderização de DOM (visão em árvore) Segundos, facilmente congela A contagem de nós explode

Então a ordem de otimização deveria ser:

  1. Reduza o volume de dados: faça a API paginar, retornar apenas os campos necessários e habilitar gzip/brotli;
  2. Evite clonagem profunda: veja a próxima seção; este é o custo oculto mais comum;
  3. Renderização preguiçosa: na visão em árvore, renderize apenas os níveis expandidos em vez de gerar dezenas de milhares de nós de DOM de uma vez;
  4. Streaming: não analise tudo de uma vez quando os dados passam de 10 MB.

Se o gargalo realmente é o JSON.parse, isso significa que os dados já têm dezenas de MB ou mais, e o movimento certo é um analisador de streaming:

import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';

// Trate os elementos do array um a um; o uso de memória independe do tamanho total
const pipeline = fs.createReadStream('big.json')
  .pipe(parser())
  .pipe(streamArray());

pipeline.on('data', ({ value }) => {
  processItem(value);
});

Um JSON de dezenas de MB derruba o navegador assim que abre. E agora?

Primeiro confirme se ele "derruba" ou "congela". Os dois precisam de tratamentos completamente diferentes:

  • Congela: a aba mostra "Página sem resposta" e pode se recuperar após dezenas de segundos. A causa são tarefas longas saturadas na thread principal;
  • Derruba: a aba vira "Ah, não", normalmente esgotamento de memória (OOM).

De qualquer forma, não o processe na thread principal. Mova a análise para um Web Worker:

// worker.js
self.onmessage = (event) => {
  const parsed = JSON.parse(event.data);
  self.postMessage(parsed);
};

Um Worker resolve "a interface não congela", não o teto de memória. Uma aba de navegador normalmente tem 1–4 GB disponíveis; um JSON de 200 MB analisado em objetos pode ocupar 1 GB ou mais (cada par chave-valor carrega overhead de objeto), e com renderização em cima chega facilmente ao limite.

A solução real é nunca carregar tudo:

Tamanho dos dados Abordagem
< 5 MB Analise com JSON.parse diretamente, cuidado com a renderização
5–50 MB Análise em Worker + rolagem virtual
> 50 MB Análise em streaming, mantenha apenas os campos necessários
> 500 MB Mude o formato (Parquet / SQLite / JSON Lines fragmentado)

JSON.parse(JSON.stringify(obj)) é uma clonagem profunda?

É uma clonagem profunda, mas ela "muda dados silenciosamente" em vários pontos, então não serve como ferramenta geral de clonagem.

const source = {
  date: new Date(),
  fn: () => {},
  undef: undefined,
  nan: NaN,
  map: new Map(),
  nested: { a: 1 },
};
JSON.parse(JSON.stringify(source));
// {
//   date: "2026-10-30T03:00:00.000Z",  // Date vira string
//   nan: null,                          // NaN vira null
//   nested: { a: 1 }                    // só este nível está correto
// }
// fn e undef desaparecem, map vira {}

A lista de problemas:

Tipo Resultado
Date Vira uma string ISO
NaN / Infinity Vira null
undefined / funções / Symbol Desaparece dentro de objetos, vira null dentro de arrays
Map / Set Vira {}
BigInt Lança TypeError
Referências circulares Lança TypeError

Navegadores modernos têm structuredClone, que suporta Date, Map, Set e referências circulares, mas não funções:

const copy = structuredClone(source);

Se for apenas um merge raso de configuração, o operador de espalhamento é a opção mais barata: { ...config, timeout: 3000 }.

Por que expandir a visão em árvore trava tanto?

Porque todo nó é um elemento DOM, e quando a contagem de nós chega às dezenas de milhares o custo de layout do navegador explode.

Um JSON de 10 MB pode conter 50k–100k nós folha. Mesmo que você renderize apenas a área visível, construir o DOM uma vez leva de centenas de milissegundos a segundos, e cada rolagem subsequente dispara um reflow.

A abordagem certa:

  1. Rolagem virtual: renderize apenas as poucas dezenas de linhas dentro do viewport e reutilize nós de DOM ao rolar;
  2. Recolher por padrão: expanda apenas os dois primeiros níveis e renderize os níveis mais profundos sob demanda;
  3. Limite a escala de renderização: acima de 10k nós, mostre "Dados grandes demais, use a busca para localizar" em vez de forçar a renderização.

É também por isso que o editor deste site degrada graciosamente em arquivos grandes — não porque ele tecnicamente não consegue renderizá-los, mas porque renderizá-los seria inutilizável.

2. Por que ele é atacado

Como funciona a injeção de __proto__?

Prototype pollution. Um atacante constrói JSON com uma chave __proto__; se seu código o mescla em um objeto sem filtrar, ele sobrescreve Object.prototype e afeta todo objeto no runtime.

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');

// Código perigoso: merge recursivo sem verificar nomes de chave
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

merge({}, payload);
({}).isAdmin;  // true — todo objeto foi poluído

O fato-chave: o JSON.parse em si não polui o protótipo; {"__proto__": ...} é analisado como uma propriedade própria comum. O que é perigoso é mesclar, atribuir, Object.assign, lodash.merge e operações semelhantes após a análise.

Três defesas:

  1. Filtre nomes de chave: pule __proto__, constructor e prototype ao mesclar;
  2. Use Object.create(null) como contêiner para que não haja cadeia de protótipos a poluir;
  3. Use um Map para armazenar pares chave-valor não confiáveis.
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }
    const value = source[key];
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      target[key] = safeMerge(target[key] ?? {}, value);
    } else {
      target[key] = value;
    }
  }
  return target;
}

Observe também que for...in percorre propriedades herdadas; Object.keys() é mais seguro.

O que devo observar ao analisar JSON não confiável?

Faça nesta ordem — baixo custo, alto retorno:

  1. Limite o tamanho: rejeite imediatamente qualquer coisa acima do limite, sem primeiro lê-la na memória;
  2. Limite a profundidade: um JSON aninhado centenas de níveis pode estourar a pilha de um analisador recursivo;
  3. Limite a contagem de chaves: evita ataques de colisão de hash e amplificação de memória;
  4. Filtre chaves perigosas: __proto__, constructor, prototype;
  5. Valide a estrutura: rode-a por um Schema para confirmar tipos e campos obrigatórios;
  6. Não confie no Content-Type: o tipo declarado e o conteúdo real podem discordar.

Uma checagem de profundidade é barata; você pode usar um filtro regex aproximado antes de analisar, ou percorrer depois de analisar:

function measureDepth(value, depth = 0) {
  if (value === null || typeof value !== 'object') {
    return depth;
  }
  let maxDepth = depth;
  for (const key of Object.keys(value)) {
    maxDepth = Math.max(maxDepth, measureDepth(value[key], depth + 1));
  }
  return maxDepth;
}

Mas esse código é ele mesmo recursivo e ainda pode estourar a pilha em dados maliciosamente profundos. A abordagem segura é impor o limite durante a análise, usando um analisador que suporte uma opção de profundidade (algumas bibliotecas fornecem um parâmetro maxDepth).

Como o XSS através de JSON acontece?

O JSON em si não é HTML e não executa. O XSS acontece no ponto em que "um valor JSON é concatenado em HTML".

Três caminhos comuns:

Caminho Exemplo Defesa
Concatenação de template no servidor res.send('<div>' + data.name + '</div>') Escape de HTML na saída
innerHTML no frontend el.innerHTML = data.html Use textContent ou sanitize
Embutir em script inline <script>var d = ${JSON.stringify(data)}</script> Escape de <, >, &

O terceiro é o mais sutil. JSON.stringify não escapa <, então dados contendo </script><script>alert(1)</script> conseguem fechar a tag script antes do fim:

const data = { name: '</script><script>alert(1)</script>' };
// Saída: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>

A correção é converter caracteres perigosos em escapes Unicode:

function safeJsonForHtml(data) {
  return JSON.stringify(data)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026')
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
}

Devo limitar a profundidade de aninhamento?

Sim. Esta é uma das defesas com a melhor relação custo-benefício.

As consequências de não limitar: funções recursivas estouram a pilha, JSON.parse derruba em algumas implementações quando a profundidade é grande demais, e a serialização estoura a pilha do mesmo jeito. Um [[[[...]]]] de 1 KB basta para derrubar um analisador desprotegido.

Um limite razoável depende do negócio:

Tipo de dado Limite sugerido
Objetos de negócio comuns 10–20 níveis
Estruturas de árvore (organogramas, diretórios) 50–100 níveis
Gateway de API genérico 20 níveis

O que é JSON hijacking?

Uma vulnerabilidade histórica: navegadores antigos permitiam que <script src> cross-origin carregasse e executasse JSON, e atacantes roubavam dados redefinindo o construtor Array ou Object.prototype.__defineSetter__.

As proteções modernas estão razoavelmente completas:

  • Navegadores não permitem mais sequestrar literais de array com __defineSetter__;
  • Um Content-Type: application/json correto impede a execução de script;
  • X-Content-Type-Options: nosniff impede o MIME sniffing.

Sistemas antigos ainda carregam risco residual. Conselho prático: use um objeto no nível superior, não um array cru. Um wrapper como {"data": [...]} impede que a resposta seja um comando JavaScript válido ({ é tratado como bloco de código), bloqueando o hijacking no nível da sintaxe.

Quais são os riscos de registrar JSON em log?

Três:

  1. Informação sensível gravada em disco: números de telefone, documentos de identidade, tokens e senhas em texto claro caem no sistema de logging, que normalmente tem permissões mais frouxas e retenção mais longa;
  2. Injeção de log: um valor JSON contendo quebras de linha e prefixos de log forjados divide um registro em vários e polui a trilha de auditoria;
  3. Explosão de tamanho: gravar uma resposta inteira de 10 MB no log enche o sistema de logging rapidamente.

Abordagem:

// Oculte e trunque antes de registrar
function safeLog(data) {
  const cloned = structuredClone(data);
  for (const key of ['password', 'token', 'idCard', 'phone']) {
    if (key in cloned) cloned[key] = '***';
  }
  const text = JSON.stringify(cloned);
  return text.length > 2000 ? text.slice(0, 2000) + '...(truncated)' : text;
}

Sistemas de logging estruturado (como o pino) escapam quebras de linha sozinhos, mas ocultar e truncar ainda é seu trabalho.

3. Referência rápida

Sintoma Verifique primeiro Correção
Análise lenta Volume de dados, se há clonagem profunda Fragmentação/streaming, remova JSON.parse(JSON.stringify())
Página congela Tarefas longas na thread principal, número de nós renderizados Worker + rolagem virtual
Aba derruba Uso de memória Não carregue tudo; mude para streaming
Clonagem profunda perde dados Date, NaN, Map Use structuredClone
Objetos de repente ganham propriedades extras Prototype pollution Filtre __proto__, use um Map
Um script aparece Strings concatenadas em HTML Escape <, use textContent
Stack overflow Profundidade de aninhamento Limite a 10–20 níveis
Logs enchem A resposta inteira é registrada Trunque + oculte