JSON Toolkit

Pare de comprimir JSON na ordem errada: minify vs gzip vs brotli, medidos

Resumo: Minificar corta 30-50% do JSON cru, mas o gzip apaga a maior parte desse ganho. Números medidos em formas reais, além dos quatro casos em que minificar piora as coisas.

Intermediário~7 min de leituraPublicado em 2026-10-31Atualizado em 2026-10-31
Compressão JSON: comparação medida de tamanho de payload entre minify, gzip e brotli

Resumo da ópera: separe primeiro a compressão de transporte do corte estrutural. Se o gzip ou o brotli ainda não está ligado, corrija a configuração do servidor — só o gzip -6 chega a 94,5% e deixa o minify valendo apenas 0,9 de um ponto. Os 46,3% completos do minify só entram em cena quando a conta de bytes é registrada localmente: corpos de requisição, os 6 MB do Lambda, o 1 MiB do Firestore, ou JSON gravado em disco e em logs.

O selo "economizou 35%" do seu minificador favorito é honesto — ele mede bytes crus. Sua API roda em cima de gzip ou brotli, e esses dois consomem exatamente a mesma redundância. Multiplique os dois números e você terá a resposta errada.

1. Separe "compressão" em duas coisas diferentes

O que as pessoas chamam de compressão JSON são, na verdade, duas operações não relacionadas, e misturá-las é como toda conclusão errada começa.

Minificar (remover espaços em branco) gzip / brotli (compressão de transporte)
Natureza Uma transformação de documento Uma transformação de transporte
Saída Ainda texto JSON válido e legível Binário; inutilizável até ser decodificado
Onde o ganho cai Disco, memória, logs, limites de tamanho Apenas no fio e em caches
Após a decodificação Os bytes permanecem menores Os bytes voltam ao tamanho original
Quem cuida Seu script de build, os argumentos do seu serializador Nginx, CDN, framework do servidor

As duas últimas linhas são as que importam. Os limites de plataforma são medidos após a decodificação: o teto de 6 MB de payload do AWS Lambda, o teto de 1 MiB por documento do Firestore. Ligar o gzip não compra um único byte ali, porque ambos contam o resultado decodificado.

2. Minificar remove exatamente uma coisa: espaço em branco estrutural

A RFC 8259 permite espaço em branco (space, \t, \n, \r) em volta de qualquer caractere estrutural. Nada mais em um documento JSON é opcional: nomes de chave são dados, números mantêm cada dígito, e o conteúdo das strings é intocável byte a byte.

Isso reduz a operação inteira a uma linha:

const minified = JSON.stringify(JSON.parse(pretty));
// A RFC 8259 permite espaço em branco apenas em volta de caracteres estruturais,
// então espaços dentro de uma string são dados, não formatação
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} - os dois espaços internos são preservados

Vale destacar três operações que são arquivadas em "compressão" mas na verdade modificam o documento: descartar chaves cujo valor é null, arredondar floats e reordenar chaves. Essas mudam o documento em vez de reformatá-lo, então qualquer ferramenta que anuncie "compressão profunda" precisa do aval do seu consumidor antes de você a habilitar.

Como o minify só remove espaço em branco, seu retorno depende quase inteiramente de quanto do seu arquivo é indentação. Espere ver isso repetidamente nas tabelas abaixo.

3. A medição: quatro documentos, três níveis de gzip, mais brotli

Ambiente: Node v22.13.1, zlib.gzipSync nos níveis 1 / 6 / 9, brotliCompressSync na sua qualidade padrão (11). As quatro amostras cobrem de propósito as formas que você realmente envia: 50 pedidos aninhados (estrutura profunda, chaves repetidas), uma configuração de 6 níveis (chaves longas, valores curtos), um array de log de 200 linhas (strings longas, quase nenhuma indentação) e o media-manifest.json real deste site (forma mista, strings com muitas URLs).

Passo um: bytes crus.

Amostra pretty (2 espaços) pretty (4 espaços) após minify minify economiza
50 pedidos aninhados 29.551 B 40.761 B 15.881 B 46,3%
Configuração de 6 níveis 25.450 B 38.012 B 11.679 B 54,1%
Array de log de 200 linhas 14.582 B 14.982 B 13.981 B 4,1%
media-manifest.json 25.149 B 28.114 B 21.222 B 15,6%

É daí que vem aquele selo "economizou 35%", e ele não está mentindo. O problema aparece no passo dois.

Passo dois: comprima cada variante e depois pergunte quanto o minify ainda acrescenta.

Amostra em cima do gzip -1 em cima do gzip -6 em cima do gzip -9 em cima do brotli
50 pedidos aninhados 21,0% 17,0% 8,9% 10,6%
Configuração de 6 níveis 74,2% 59,1% 58,0% 56,4%
Array de log de 200 linhas -0,7% -0,4% 0,6% -0,3%
media-manifest.json 1,5% 1,0% 1,0% 1,6%

O mesmo documento de pedidos aninhados que perde 46,3% dos seus bytes crus mantém apenas 17,0% disso quando o gzip -6 entra em jogo. O array de log fica até negativo: minificar e depois comprimir é ligeiramente maior do que comprimir o original formatado. Negativo não é bug, é exatamente o que você deveria esperar, e a próxima seção explica por quê.

Passo três: redução total contra o documento formatado original — o número que realmente trafega pelo fio.

Amostra Apenas gzip -6 minify + gzip -6 minify + brotli
50 pedidos aninhados 94,5% 95,4% 96,9%
Configuração de 6 níveis 96,0% 98,4% 99,0%
Array de log de 200 linhas 89,9% 89,9% 95,0%
media-manifest.json 83,5% 83,6% 86,1%

4. Por que o gzip engole o crédito do minify

O gzip é LZ77 seguido de codificação de Huffman. O movimento central do LZ77 é "olhe para trás em uma janela de 32 KB, encontre uma sequência repetida, substitua-a por um ponteiro (distância, comprimento)". A indentação é o alvo mais fácil possível para esse movimento: cada nível de aninhamento emite uma sequência idêntica de espaços, e dezenas de linhas adjacentes repetem a mesma combinação. O Huffman então atribui códigos curtos a símbolos de alta frequência, então um padrão como \n mais oito espaços — um que pode se repetir mil vezes em um único arquivo — acaba custando dois ou três bits.

Então os bytes que o minify exclui são precisamente os bytes que o compressor teria colapsado quase de graça. Fazê-lo você mesmo significa refazer um trabalho em que o compressor já é excelente, e seus 46% colapsam para 17%. Essa é toda a história por trás da tabela acima.

O resultado negativo do array de log merece sua própria explicação. Essa amostra é quase inteiramente strings longas (linhas de log, URLs, base64), o que significa poucas repetições e alta entropia — pouca redundância para o compressor remover em primeiro lugar. As quebras de linha na versão formatada por acaso dão ao matcher fronteiras estáveis. Remova-as e o LZ77 perde essas âncoras: nesta amostra, comprimir o original formatado no gzip -1 produz 1.508 bytes, enquanto minificar e comprimir produz 1.518 bytes. Dez bytes pior.

Ordenadas por magnitude, as alavancas se alinham assim:

Alavanca Economia vs. o documento formatado original (pedidos aninhados) Custo
minify 46,3% (apenas bytes crus) Legibilidade perdida; precisa de uma etapa de build
gzip -6 94,5% Um pouco de CPU do servidor; configuração quase nula
minify + gzip -6 95,4% As duas acima, somadas
minify + brotli 96,9% Precisa de HTTPS e do módulo brotli; pré-comprima ativos estáticos

O salto de 0 para 94,5% é trabalho do gzip. O salto de 94,5% para 95,4% — nove décimos de um ponto — é do minify. Esta tabela decide onde seu esforço pertence.

5. O nível de compressão é a variável oculta

Qualquer argumento que presume um único nível fixo de gzip está em terreno arenoso, porque o nível muda diretamente o valor marginal do minify:

Configuração de transporte Bytes extras que o minify ainda economiza (pedidos aninhados)
gzip -1 (padrão do gzip_comp_level no Nginx) 21,0%
gzip -6 (padrão na maioria das bibliotecas padrão) 17,0%
gzip -9 8,9%
brotli (qualidade padrão) 10,6%

Quanto mais fraca sua compressão de transporte, mais o minify compensa. Isso lhe dá uma regra prática: em uma instalação padrão do Nginx onde gzip_comp_level ainda é 1, o minify entrega 21%. Uma vez ajustado para 9 ou trocado para brotli, cai para 9-11% e você normalmente pode pular.

Antes de argumentar para qualquer lado, meça em qual faixa você está realmente:

# Busque a mesma resposta com três valores de Accept-Encoding e compare o que chegou
for enc in identity gzip br; do
  size=$(curl -s -o /dev/null -w '%{size_download}' \
    -H "Accept-Encoding: $enc" https://api.example.com/v1/orders)
  echo "$enc -> $size bytes"
done

Três números respondem a três perguntas de uma vez: a compressão está sequer ligada, em que nível, e quanto espaço resta para o minify. Se identity e gzip voltam quase idênticos, a compressão não está ativa de forma alguma — e a coisa a corrigir é o Nginx, não o seu JSON:

gzip on;
gzip_comp_level 6;
gzip_min_length 256;      # pule respostas minúsculas, elas só crescem
gzip_types application/json application/javascript text/css;

6. Quando o minify realmente compensa

Um teste decide: os bytes que você economizou serão registrados em algum lugar, ou você baterá em um limite que conta bytes crus? Se sim, os 46% completos entram. Se a resposta é "eles só existem no fio", então 17% é o valor real.

Primeiro, corpos de requisição HTTP. A maioria dos servidores não descomprime corpos de requisição, e gzip é configurado quase exclusivamente para respostas. O JSON que você faz POST normalmente não tem proteção de camada de transporte nenhuma, então as economias do minify chegam a preço cheio. O mesmo vale para callbacks de webhook, telemetria MQTT e frames de WebSocket que nunca negociaram permessage-deflate.

Segundo, canais com um teto rígido de bytes — e todos contam bytes decodificados:

Canal Limite O gzip ajuda?
Payload síncrono do AWS Lambda 6 MB Não, medido após a decodificação
Documento único do Firestore 1 MiB Não
Mensagem única do Kafka (padrão) 1 MB Não, a compressão no broker é outra camada
Cota do localStorage ~5 MB (contados em caracteres) Não
URL de requisição GET Depende do servidor, 8 KB é comum Não

Nesta categoria o minify não é uma otimização, é a diferença entre uma requisição que é enviada e uma que falha. Embutir JSON em uma query string de URL é o caso mais agudo: URLs nunca são comprimidas e têm um limite de comprimento, então minifique primeiro e depois decida se você precisa mesmo de base64.

Terceiro, JSON gravado em disco ou em logs. Logs e índices armazenam bytes literais sem camada de transporte envolvida. Há uma ressalva que pega as pessoas: o minify só ajuda se você já estava gravando JSON formatado. Aquela amostra de log de 200 linhas economizou apenas 4,1%, porque suas linhas já eram compactas e a indentação não representava quase nada do arquivo. Inversamente, se seu logger chama JSON.stringify(obj, null, 2), remover o terceiro argumento é dinheiro de graça.

Uma última observação: quando os bytes crus realmente estão apertados, os nomes de chave costumam ser uma alavanca maior que a indentação. No media-manifest.json, substituir suas 14 chaves únicas por k0 a k13 leva a forma minificada de 21.222 B para 18.887 B — outros 11,0%. Comprimido, essa margem restante encolhe para 2,4% sob gzip -6 e 1,3% sob brotli. O mesmo mecanismo de tudo acima: indentação e nomes de chave são ambos informação estrutural repetida, e o compressor reivindica o mesmo dividendo em qualquer caso.

7. Quatro casos em que dá errado

Não minifique arquivos que você commita. Mude "qty": 2 para 3 e o arquivo formatado mostra uma linha limpa no git diff; o arquivo minificado reescreve a linha inteira, e com centenas de campos você não consegue achar a mudança. É a mesma mecânica coberta em Como resolver conflitos de merge em JSON — ferramentas de diff operam por linha, então JSON de uma linha transforma toda edição em "a linha inteira difere". Mantenha arquivos formatados no repositório e minifique apenas na saída de build ou ao gravar em armazenamento.

Não comprima respostas minúsculas. Uma resposta contendo apenas {"ok": true} tem 16 bytes quando a indentação é adicionada, e 36 bytes após o gzip -6 — vinte bytes maiores, porque o dicionário e os cabeçalhos do gzip ocupam espaço. Isso não é teórico; é exatamente por isso que gzip_min_length existe. Seu padrão de 20 bytes no Nginx é baixo demais para produção, onde 256 é o piso usual.

Não espere análise mais rápida. Escalando o mesmo conjunto de pedidos para 4.000 registros, o documento formatado tem 2.374.378 bytes contra 1.282.358 minificados — 46% menor — e ainda assim o tempo mediano de JSON.parse só vai de 5,7 ms para 5,0 ms, cerca de 12% mais rápido. Reduzir o tamanho pela metade não reduz o tempo pela metade, porque o analisador gasta a maior parte do orçamento fatiando strings e convertendo números; pular espaços em branco é incidental. O que realmente determina o custo de análise é a contagem de chaves e o conteúdo das strings, o que Análise, renderização e streaming de arquivos JSON grandes detalha minuciosamente.

Não o adapte a um serviço que já roda brotli ou gzip -9. O retorno cai para 9-11%, enquanto o custo — perda permanente de legibilidade ao depurar — permanece. Verifique em vez disso se gzip_types cobre mesmo application/json. Muitos sites habilitam o gzip e só comprimem text/html, deixando o JSON completamente sem compressão; nessa situação o minify não é o primeiro passo, corrigir sua configuração é.

8. Tabela de decisão: qual passo primeiro

Sua situação Faça isto primeiro Onde o minify fica
Nenhuma compressão de transporte Configure gzip_types no Nginx ou habilite a auto-compressão da CDN Pule; nove décimos do ganho evaporam
Rodando gzip -1 (padrão do Nginx) Suba gzip_comp_level para 6 Segundo, vale 21%
Já em brotli ou gzip -9 Audite chaves longas, adicione paginação ou corte campos Pule; sobram só 9-11%
Batendo nos 6 MB do Lambda ou no 1 MiB do Firestore Minifique direto; os 46% completos contam Primeiro
Corpos POST, webhooks, MQTT Minifique direto; o transporte raramente se aplica aqui Primeiro
Arquivos de configuração indo para o Git Mantenha-os formatados Apenas na saída de build
Logs, telemetria, dumps offline Remova o argumento null, 2 do seu logger Isso é o passo um

A regra de uma frase: olhe qual livro-caixa de bytes você está tentando encolher — um registrado localmente (disco, cota, limite de plataforma, corpo de requisição) ou um que só existe no fio. Local primeiro significa minify; apenas no fio significa ligar brotli ou gzip primeiro.

Na prática, rode o laço curl acima para confirmar que a compressão de transporte está ativa e depois passe uma amostra pela ferramenta de minificação JSON e leia seu número de economia. Se esse número estiver acima de 40% enquanto o curl relata identity e gzip com tamanhos quase iguais, seu gargalo é a configuração do servidor, não o JSON. Se os dois números do curl já estão próximos, este documento tem pouco a oferecer e você deveria estar cortando campos ou paginando em vez disso — o assunto de Fundamentos do projeto de contrato de API JSON.

JSON minificado ainda é JSON válido?

Sim. O minify remove apenas o espaço em branco que a RFC 8259 permite em volta de caracteres estruturais e nunca toca no conteúdo das strings, então JSON.parse produz um valor idêntico ao original. A única vítima é a legibilidade humana.

brotli ou gzip?

Na mesma amostra minificada, o brotli economiza 96,9% e o gzip -6 economiza 95,4% — uma diferença estreita. Use gzip -6 para respostas dinâmicas para manter a CPU baixa, e reserve o brotli para ativos estáticos entregues por HTTPS que você consiga pré-comprimir em arquivos .br.

Minifiquei meu JSON mas a API não ficou mais rápida. Por quê?

Verifique a resposta com curl -sI em busca de um cabeçalho Content-Encoding: gzip. Muitas configurações do Nginx deixam gzip_types apenas em text/html, então o JSON nunca foi coberto — e menos de um quinto dos seus 46% chegou ao fio.

Arquivos de configuração devem ser commitados minificados?

Não. Ferramentas de diff comparam linha por linha, então JSON de uma linha transforma toda edição em "a linha inteira mudou", e resolver conflitos custa muito mais do que os bytes que você economizou. Mantenha versões formatadas no repositório e minifique em tempo de build.

Quão grande uma resposta deve ser antes de o gzip valer a pena?

O limite usual é 256 bytes a 1 KB. Uma resposta tão pequena quanto {"ok": true} — 16 bytes com indentação — cresce para 36 bytes sob gzip -6, porque o dicionário e os cabeçalhos custam mais do que recuperam. É precisamente isso que gzip_min_length controla.