Ir para o conteúdo
OneKitly

O que um minificador pode remover e o que não pode tocar

Publicado a 19/05/2025 · 18 min de leitura · Ferramentas para programadores

Daniel Okonkwo

Daniel OkonkwoProgramador front-end e redator de Tecnologia na OneKitly

Desempenho web · Formatos de ficheiro

Verificado a partir de 6 fontes

Ver perfil
Em resumo

Um minificador só pode fazer alterações que preservem a semântica, e os casos difíceis são todos espaços que não são decoração. Em CSS, o espaço de «div p» é um combinador descendente: retire-o e o seletor passa a corresponder a algo completamente diferente. Os espaços à volta de >, + e ~ podem sair — o esbuild converteu «.card > footer» em «.card>footer» deixando «.card .card-title» intacto. Dentro de calc(), os espaços à volta de + e − são obrigatórios: o Chrome reporta CSS.supports para calc(100% - 2px) como true e para calc(100%-2px) como false, e atribuir o segundo deixa a propriedade vazia. Os espaços à volta de * e / são opcionais. Em HTML, o espaço entre elementos em linha é conteúdo renderizado — dois spans mediram 36,92 px com um espaço entre eles e 27,28 px sem, um desvio de 9,64 px — e é plenamente significativo dentro de pre e textarea. Em JavaScript, a inserção automática de ponto e vírgula faz com que juntar linhas altere o comportamento: uma função cujo return está sozinho na sua linha devolve undefined até a juntar, altura em que devolve o objeto. E a renomeação para na fronteira das cadeias: o mangling de propriedades transformou uma leitura que funcionava em undefined. Quanto ao ganho, nos recursos desta página o brotli sozinho poupou 66,2 % e minificar antes acrescentou apenas 18,1 %.

A minificação tem de preservar o significado, e o interessante são os espaços que carregam significado: o combinador descendente, os espaços dentro de calc(), o intervalo entre dois elementos em linha. Medido aqui em ficheiros reais, incluindo o que o brotli já lhe ia poupar.

A única regra: a saída tem de se comportar de forma idêntica

A minificação é um passo de compilação com um só contrato: a saída tem de ser observacionalmente idêntica à entrada. Não parecida, não suficientemente próxima — idêntica em todo o comportamento de que uma página possa depender. Tudo o que um minificador faz decorre daí, e todo o erro de minificação é um sítio onde alguém assumiu que um byte era decoração quando a especificação diz que é dado.

Essa distinção explica porque os minificadores por expressões regulares são perigosos e os baseados em analisador não são. Uma ferramenta que retira espaços por padrão não sabe se um dado espaço separa dois tokens por legibilidade ou junta dois tokens num significado composto. Uma ferramenta que tokeniza a entrada segundo o módulo CSS Syntax ou a gramática ECMAScript, constrói uma árvore e a reemite não consegue cometer o erro: quando imprime seja o que for, o significado já está fixado na árvore.

Tudo o que se segue foi executado e não recordado. As amostras foram minificadas com o esbuild, os tamanhos medidos com o zlib do node em gzip nível 9 e brotli qualidade 11, e as afirmações de CSS e de layout verificadas no Chrome sem interface.

CSS: o espaço que é um combinador

Num seletor, o espaço entre dois seletores compostos é o combinador descendente. «div p» seleciona todo o p dentro de um div; «divp» seleciona um tipo de elemento inexistente, e «.card .card-title» seleciona um .card-title dentro de um .card enquanto «.card.card-title» seleciona um elemento que traz ambas as classes. O espaço é um token, não formatação, e nenhum minificador correto o retira.

Os outros três combinadores são pontuação e os espaços à sua volta são gratuitos. Dando ao esbuild as quatro formas «div p», «div>p», «div + p» e «div ~ p», devolveu exatamente «div p», «div>p», «div+p» e «div~p». O espaço descendente sobreviveu; os que rodeiam >, + e ~ não, porque esses carateres são inequívocos por si sós. Na folha de exemplo aconteceu o mesmo com regras reais: «.card .card-title» passou intacto enquanto «.card > footer», «.card + .card» e «.card ~ .aside-note» se apertaram.

O caso das regras-at é mais subtil e merece atenção. A consulta de media «@media (min-width: 600px) and (max-width: 900px)» saiu como «@media(min-width:600px)and (max-width:900px)». O espaço antes de «and» desapareceu, porque um parêntese de fecho já termina o token anterior. O espaço depois de «and» ficou, porque «and(» seria tokenizado como token de função em vez de identificador seguido de parêntese. Toda a disciplina cabe numa linha: um espaço é removível exatamente quando os dois tokens de cada lado não podem fundir-se num token diferente.

CSS: os espaços dentro dos valores

O calc() é o exemplo mais nítido, porque a exigência é assimétrica. A especificação CSS Values impõe espaços de ambos os lados de + e −, pois sem eles um token como «-2px» é lido como uma única dimensão negativa e a expressão perde o seu operador. O Chrome concorda com precisão: CSS.supports para width e calc(100% - 2px) devolve true, enquanto calc(100%-2px), calc(100% -2px) e calc(100%- 2px) devolvem todos false. Atribuir style.width = «calc(100%-2px)» deixa a propriedade como cadeia vazia, porque a declaração inteira é descartada como inválida.

Os operadores de multiplicação e divisão não têm esse problema, e o navegador confirma-o: calc(100%*2) e calc(100%/2) devolvem ambos true. Um minificador que entendesse a gramática poderia, portanto, apertar esses dois e não os outros. Na prática o esbuild é conservador e manteve cada espaço de «calc(100% - 2 * var(--gap))» — correto para o menos, e uma pequena oportunidade perdida para o asterisco.

Outras duas categorias de espaço intocável surgiram na mesma execução. Os valores de cadeia são literais: a declaração content: « new » manteve ambos os pares de espaços interiores, porque esses carateres são inseridos no documento. E as propriedades personalizadas são fluxos de tokens e não valores analisados, pelo que o esbuild deixou «--card-bg: #ffffff» com o espaço a seguir aos dois-pontos enquanto retirava o espaço idêntico de «color: var(--card-fg)». O resto da amostra mostra o que um minificador ganha quando percebe a gramática dos valores: rgba(0, 0, 0, 0.08) passou a #00000014, #0000ff a #00f, 150ms a .15s, opacity 0.7 a .7, margin: 0 0 8px 0 a margin:0 0 8px, e ::after a :after.

HTML: o espaço entre elementos em linha é conteúdo

O processamento de brancos do CSS reduz uma série de espaços em fluxo normal a um único espaço — mas um espaço, não nada. Entre dois elementos de nível linha esse espaço é renderizado e ocupa largura, pelo que apagá-lo desloca o layout. Medido no Chrome sem interface a monospace de 16 px, dois spans adjacentes separados por uma quebra de linha na fonte terminavam em x = 36,92, enquanto os mesmos dois spans escritos sem branco entre eles terminavam em x = 27,28. A diferença de 9,64 px é exatamente um carácter de espaço, e é a diferença entre uma fila de ligações que lê «one two three» e outra que lê «onetwothree».

É por isso que os minificadores HTML agressivos são configuráveis e os seus valores por omissão costumam ser cautelosos. Reduzir cinco espaços e duas quebras de linha a um espaço é sempre seguro em fluxo normal. Apagar o último espaço restante entre duas caixas em linha não é, e um minificador que o faça como regra geral irá refluir em silêncio menus, trilhos de navegação, listas de etiquetas e ícones em linha. Qualquer ferramenta que ofereça remover os brancos entre etiquetas está a oferecer mudar o seu layout a troco de bytes.

Dois elementos estão absolutamente vedados: pre e textarea. Ambos valem por omissão white-space: pre, pelo que cada espaço, tabulação e quebra de linha lá dentro é preservado e renderizado. A página de exemplo contém um bloco de código indentado e um textarea com espaços iniciais significativos, e o colapsador de brancos usado para a medição teve de receber uma exceção explícita para ambos. Qualquer minificador sem essa exceção destrói em silêncio exemplos de código e campos de formulário pré-preenchidos. O mesmo cuidado vale dentro dos elementos script e style, e para a quebra de linha inicial logo após uma etiqueta pre de abertura, que o analisador HTML descarta por especificação — subtileza que deixa as ferramentas caseiras erradas nos dois sentidos.

JavaScript: inserção automática de ponto e vírgula e renomeação

O ECMAScript insere pontos e vírgulas em certas quebras de linha, o que torna uma quebra de linha portadora de sentido. O caso canónico é um return sozinho na sua linha. Executar function f(){ return \n { ok: true } } devolveu undefined, porque é inserido um ponto e vírgula logo depois de return. Escrever o mesmo código numa linha devolveu { ok: true }. Um minificador ingénuo que junte linhas altera, portanto, o valor que a função produz. O inverso também morde: o excerto let x = 1 \n ++x avalia x como 2, enquanto juntar essas duas linhas lança SyntaxError: Invalid left-hand side expression in postfix operation.

Um minificador baseado em analisador não consegue cometer nenhum dos dois erros, porque quando imprime o ponto e vírgula já está decidido. Dando a mesma função com o return isolado ao esbuild obtém-se function t(){}export const r=void 0; — manteve a semântica, viu que a função só podia devolver undefined e dobrou a chamada para void 0. É essa a diferença entre uma transformação de texto e um compilador.

O outro perigo do JavaScript é a renomeação, e a sua fronteira é exata: um minificador pode renomear tudo aquilo cujas referências vê todas, e mais nada. As variáveis locais e os parâmetros de função qualificam-se, e é daí que vem a maior parte da poupança. Os nomes de propriedades de objeto não, porque uma propriedade pode ser alcançada por uma cadeia que o minificador não consegue seguir. Demonstração: um módulo que devolvia [config.userName, o[«userName»], o[«retryCount»]] deu [«ada», «ada», 3] com minificação simples, e [«ada», undefined, undefined] assim que o mangling de propriedades foi ligado. O acesso por ponto foi renomeado com a definição; as duas leituras por cadeia continuavam a pedir os nomes antigos e não encontraram nada. A mesma armadilha apanha tudo o que é alcançado por nome em tempo de execução — acesso por parênteses retos construído a partir de uma variável, idas e voltas por JSON, ligações de framework e eval direto.

Medido: quanto vale a minificação depois da compressão

A minificação e a compressão removem redundância sobreposta, pelo que a segunda a correr parece sempre menos impressionante. Nas amostras escritas à mão: o CSS passou de 1 434 para 1 066 bytes, menos 25,7 %, mas depois do brotli o par era 552 contra 459 — apenas 16,8 %. O JavaScript passou de 1 518 para 747 bytes, menos 50,8 %, mas 552 contra 395 depois do brotli, 28,4 %. O HTML passou de 1 033 para 782 bytes, 24,3 %, e 322 contra 302 depois do brotli — 6,2 %, ou vinte bytes.

Juntar os três numa carga de página põe o número honesto na mesa. Os recursos em bruto somam 3 985 bytes e o brotli leva-os a 1 346 — 66,2 % de poupança só pela compressão, sem passo de build. Minificar primeiro e comprimir depois dá 1 102 bytes. Portanto a contribuição marginal da minificação, por cima de uma camada de compressão que já tem, é de 244 bytes: 18,1 %. Real e digna de se aproveitar, mas uma ordem de grandeza abaixo do que o número em bytes brutos sugere.

Quatro ficheiros reais deste repositório mostram quanto a resposta depende do que está lá dentro. O globals.css encolheu 68,2 % em bruto e 70,2 % depois do brotli — espetacular, e explicado por inteiro pelo facto de 2 084 dos seus 3 393 bytes serem comentários, com os treze blocos de regras intactos dos dois lados. O app-shell.module.css, que é sobretudo declarações reais, deu 4,4 % em bruto e 5,8 % depois do brotli. Dois módulos TypeScript carregados de conteúdo deram 10,8 % e 6,5 % em bruto, mas apenas 3,4 % e 2,3 % depois do brotli, porque um ficheiro feito sobretudo de cadeias literais quase não tem nada que um minificador possa tocar. Regra prática: a minificação paga na proporção do que no seu ficheiro são comentários, indentação e identificadores locais longos, e não paga nada sobre dados.

Uma ordem de operações que funciona

Ligue primeiro a compressão, porque é a maior vitória isolada, não precisa de passo de build e não pode partir nada. Nestas amostras o brotli sozinho retirou 66,2 % dos bytes. Depois minifique com uma ferramenta baseada em analisador para cada linguagem, nas suas definições por omissão. A seguir, só se tiver uma razão medida, vá às opções agressivas — mangling de propriedades, remoção de brancos entre etiquetas — e trate cada uma como uma alteração que precisa de testes, porque cada uma é um sítio onde o contrato de preservação semântica foi deliberadamente afrouxado.

Dois hábitos valem mais do que qualquer definição de minificador. Retire comentários e código morto na origem em vez de confiar que o minificador dê por eles — o globals.css era 61 % comentários, e esse único facto explica toda a sua redução de 68 %. E verifique a saída, não a promessa: passe o pacote minificado pela sua bateria de testes e compare os tamanhos comprimidos dos dois lados em vez dos brutos, porque o número bruto é o que lisonjeia e o comprimido é o que os seus utilizadores realmente descarregam.

Transformações passadas pelo esbuild na folha e no módulo de exemplo, com o veredicto de cada uma
AntesDepoisSeguro?Porquê
.card > footer.card>footerSimO > é inequívoco por si só; os espaços não carregam nada
.card .card-title.card .card-title (inalterado)Não pode mudarO espaço é o combinador descendente; retirá-lo seleciona um elemento com ambas as classes
calc(100% - 2px)calc(100% - 2px) (inalterado)Não pode mudarO Chrome reporta calc(100%-2px) como não suportado e descarta a declaração
content: « new »content:« new » (espaços mantidos)Não pode mudarO conteúdo das cadeias é inserido tal e qual no documento
rgba(0, 0, 0, 0.08)#00000014SimO hexadecimal de oito dígitos representa exatamente a mesma cor
margin: 0 0 8px 0margin:0 0 8pxSimA abreviatura espelha o segundo valor quando o quarto é omitido
return sozinho na sua linhaquebra de linha retirada por uma ferramenta de textoNãoA função devolvia undefined antes e o objeto depois
o.userName e o[«userName»]o mangling de propriedades renomeia só o primeiroNãoO resultado medido passou de [ada, ada, 3] para [ada, undefined, undefined]
Compressor de CSSMinifique CSS removendo comentários e espaços para reduzir o tamanho da folha de estilos.Experimentar a ferramenta

Perguntas frequentes

Se o meu servidor já envia gzip ou brotli, ainda é preciso minificar?
Sim, mas espere um ganho muito menor do que os números em bruto sugerem, e ponha primeiro a compressão a funcionar. Medido na página de exemplo, o brotli sozinho levou 3 985 bytes de recursos em bruto a 1 346 — 66,2 % poupados sem qualquer passo de build. Minificar antes de comprimir chegou a 1 102 bytes, pelo que a contribuição marginal da minificação foi de 244 bytes, ou 18,1 % por cima. Vale a pena, e não custa nada por pedido depois de o seu build o fazer. Os dois sobrepõem-se porque atacam a mesma redundância: identificadores longos repetidos, sequências de indentação e texto de comentários são exatamente aquilo que um compressor de dicionário melhor elimina. Onde a minificação ganha de forma clara é naquilo que a compressão não pode fazer, por ser semântico e não textual — eliminação de código morto, dobragem de constantes, remoção de ramos inalcançáveis, encurtamento da sintaxe de cores e unidades. A ordem que importa: compressão ligada, depois minificar, depois medir os tamanhos comprimidos e não os brutos.
O meu layout deslocou-se depois de ligar a minificação HTML. Porquê?
Quase de certeza porque o minificador retirou brancos entre elementos de nível linha, que são conteúdo renderizado e não formatação. O processamento de brancos do CSS reduz uma série de espaços e quebras de linha em fluxo normal a um espaço — um, não zero — e esse espaço sobrevivente ocupa largura entre duas caixas em linha. Medido no Chrome sem interface a monospace de 16 px, dois spans com uma quebra de linha entre eles na fonte terminavam em x = 36,92, e o mesmo par sem qualquer branco em x = 27,28: 9,64 px de diferença, exatamente um espaço. Numa barra de navegação, numa lista de etiquetas ou numa fila de ligações em linha, essa diferença vê-se de imediato e parece um erro. Procure uma opção do género collapseWhitespace com um modo agressivo ou conservador, e prefira o conservador. Se não quiser intervalo entre dois elementos em linha, retire-o em CSS com um contentor flex ou grid, ou com font-size no pai, para que a marcação continue independente do layout.
Alguma vez é seguro renomear propriedades de objeto?
Só quando puder garantir que todo o acesso à propriedade é visível para o minificador, o que na prática significa adotar uma convenção de nomes e informar a ferramenta. O padrão comum é mangling apenas das propriedades que correspondem a um padrão, como um sublinhado final, para que os campos internos sejam renomeados e tudo o que é público fique intacto. O que o parte é qualquer acesso por cadeia. Demonstrado aqui: um módulo que devolvia [config.userName, o[«userName»], o[«retryCount»]] deu [«ada», «ada», 3] com minificação normal e [«ada», undefined, undefined] assim que o mangling de propriedades foi ativado — o acesso por ponto acompanhou a definição, as duas leituras por cadeia não. O mesmo modo de falha cobre o acesso por parênteses retos construído a partir de uma variável, as chaves vindas de JSON, os modelos de framework que ligam por nome e tudo o que seja percorrido com Object.keys. Como a quebra é silenciosa e só aparece no caminho de código que usa a cadeia, trate o mangling de propriedades como uma otimização que exige uma bateria completa de testes, e não como uma caixa a assinalar.
Posso escrever um minificador com expressões regulares?
Pode escrever algo que normalmente funciona, o que é o pior resultado possível, porque as falhas são raras e silenciosas. Um padrão que retira sequências de brancos não distingue um combinador descendente de uma indentação, não sabe que o espaço antes de um menos dentro de calc() é gramaticalmente obrigatório, não vê que uma quebra de linha antes de uma chaveta de fecho é o que faz um return devolver undefined, e não sabe que os carateres entre aspas são conteúdo. Cada uma dessas distinções exige tokenizar a entrada segundo a gramática real. O colapsador de brancos usado para a medição HTML deste artigo é deliberadamente ingénuo, e foi preciso talhar-lhe uma exceção explícita para pre e textarea antes de sequer produzir saída correta — e continuaria a não ser seguro numa página com scripts em linha que contenham sinais de menor dentro de cadeias. A resposta prática: use uma ferramenta baseada em analisador por linguagem e gaste o seu esforço na entrada — menos comentários enviados, nada de código morto, nomes locais mais curtos onde não prejudiquem a legibilidade.
Porque é que minificar o meu ficheiro cheio de dados quase não ajudou?
Porque um minificador só pode tocar na sintaxe, e um ficheiro de dados é quase todo conteúdo. As cadeias literais têm de sobreviver byte a byte, as chaves de objeto usadas em tempo de execução não podem ser renomeadas, e os números já são o mais curtos que vão ser. As medições deste artigo mostram o padrão com clareza: dois módulos TypeScript cheios de conteúdo deste repositório encolheram 10,8 % e 6,5 % em bruto, mas apenas 3,4 % e 2,3 % depois do brotli, porque o pouco que a minificação retirou era indentação e pontuação que o compressor ia espremer de qualquer forma. Compare com o globals.css, que encolheu 68,2 % — inteiramente porque 2 084 dos seus 3 393 bytes eram comentários. Se um ficheiro de dados é mesmo grande, a alavanca não é a minificação mas o formato e a entrega: ponha os dados atrás de uma API para que as páginas tragam só o que mostram, divida-os para que uma rota carregue a sua própria fatia, ou tire-os do pacote JavaScript para JSON que o navegador analisa mais depressa e guarda em cache à parte.

Artigos que podem interessar-lhe

Todos os guias
GuiaEmbelezar ou minificar: para que serve cada um e o que faz ao pesoQuatro folhas de estilo reais passadas pelo minificador, medidas em bruto e depois de gzip. Retirar todos os espaços poupou 48, 103, 104 e 147 bytes comprimidos; retirar os comentários, 57, 1 358, 2 420 e 1 042. E as cinco entradas que este minificador parte.TutorialHEX vs RGB: como ler e converter coresHEX e RGB são duas notações para as mesmas cores. Eis como ler cada uma, converter entre elas e adicionar transparência.ExplicaçãoGradientes, bandas e porque o meio parece turvoInterpolar em sRGB faz a média de números codificados em gama: o ponto médio de vermelho para verde é #808000 quando a resposta a meia-luz é #bcbc00 — menos 57,2 % de luz. As bandas são outro problema aritmético: 8 bits dão 256 degraus, e um gradiente escuro pode ter apenas 28. Eis ambos, calculados.ExplicaçãoOs esquemas de cor são geometria numa roda — e a roda está erradaComplementar, tríade, análogo e complementar dividido são apenas rotações: some 180°, 120°, 30° ou 150° a um matiz. A aritmética é trivial. O problema é que o círculo de matizes HSL não é percetualmente uniforme — o amarelo e o azul com a mesma luminosidade HSL diferem 12,85 vezes em luminância — por isso uma paleta gerada tem de ser verificada em contraste depois.ExplicaçãoRácio de contraste: como a WCAG o calcula realmenteO rácio WCAG é (L1 + 0,05) ÷ (L2 + 0,05), e L é a luminância relativa, não o brilho. O verde carrega 71,52 % e o azul 7,22 %, razão pela qual o azul puro sobre branco passa com 8,59:1 enquanto o cinzento médio falha com 3,95:1. Eis o cálculo inteiro, corrido de ponta a ponta.ExplicaçãoComo funcionam de verdade as fontes do Instagram (não são fontes)Nada é estilizado. Cada letra é trocada por outro carácter Unicode que por acaso se parece com negrito ou cursivo — daí um leitor de ecrã ler uma algaraviada e alguns aparelhos mostrarem quadrados vazios.

Ferramentas relacionadas

Fontes

Detetaste um erro neste artigo?