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.

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:
- Reduza o volume de dados: faça a API paginar, retornar apenas os campos necessários e habilitar gzip/brotli;
- Evite clonagem profunda: veja a próxima seção; este é o custo oculto mais comum;
- 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;
- 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:
- Rolagem virtual: renderize apenas as poucas dezenas de linhas dentro do viewport e reutilize nós de DOM ao rolar;
- Recolher por padrão: expanda apenas os dois primeiros níveis e renderize os níveis mais profundos sob demanda;
- 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:
- Filtre nomes de chave: pule
__proto__,constructoreprototypeao mesclar; - Use
Object.create(null)como contêiner para que não haja cadeia de protótipos a poluir; - Use um
Mappara 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:
- Limite o tamanho: rejeite imediatamente qualquer coisa acima do limite, sem primeiro lê-la na memória;
- Limite a profundidade: um JSON aninhado centenas de níveis pode estourar a pilha de um analisador recursivo;
- Limite a contagem de chaves: evita ataques de colisão de hash e amplificação de memória;
- Filtre chaves perigosas:
__proto__,constructor,prototype; - Valide a estrutura: rode-a por um Schema para confirmar tipos e campos obrigatórios;
- 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/jsoncorreto impede a execução de script; X-Content-Type-Options: nosniffimpede 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:
- 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;
- 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;
- 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 |