Minificador XML
Remove o espaço insignificante. O conteúdo misto fica intacto.
Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.
Cole um documento acima e o espaço em branco entre os elementos é removido. O resultado volta como uma única linha ao lado da sua entrada, com o tamanho antes, o tamanho depois e a porcentagem economizada. Os comentários também saem se você marcar a caixa. Tudo roda nesta aba: o analisador e o minificador são JavaScript, e o seu documento nunca é enviado para lugar nenhum.
Minifique quando a restrição for o tamanho do próprio documento. Centenas de milhares de payloads numa coluna de banco de dados, um envelope embutido num campo de texto JSON, uma mensagem que precisa caber no limite de tamanho de um broker. Pela rede vale muito menos do que as porcentagens de manchete sugerem, e os números abaixo dizem por quê.
A regra é a do formatador, invertida: espaço em branco entre elementos é layout, espaço dentro de conteúdo misto é dado. Quando um elemento contém texto e elementos filhos ao mesmo tempo, a subárvore dele é tirada da sua fonte em vez de reconstruída, e o CDATA é copiado byte a byte. O que sobra não pode mudar o que um analisador entrega à aplicação.
O que é seguro remover
Um analisador XML entrega à aplicação cada caractere do documento, espaço em branco incluído; não há nenhum que ele descarte por você. "Insignificante" é uma afirmação sobre o modelo de conteúdo, não sobre o analisador: se uma DTD ou um esquema declara que um elemento tem conteúdo só de elementos, o espaço diretamente dentro dele não pode ser dado, e esse é o único espaço que um minificador pode tocar.
A maioria dos documentos chega sem esquema, então o minificador aplica uma heurística, e a daqui é estreita: um nó de texto composto inteiramente de espaço em branco, entre elementos irmãos, é descartado. Nada mais. É o mesmo julgamento que o libxml2 faz com --noblanks. Mais uma mudança: um elemento vazio é escrito autofechado, então <status></status> vira <status/>. Qualquer analisador conforme vê o mesmo elemento, mas não são os mesmos bytes.
O que não é removido
Conteúdo misto é o que separa um minificador de verdade de uma expressão regular. Em <p>Olá <b>mundo</b>!</p> o espaço depois de "Olá" é um caractere do documento, e o espaço entre </b> e <i> em <p>a <b>x</b> <i>y</i></p> também. Os dois sobrevivem, porque uma subárvore mista é copiada literalmente da sua fonte em vez de reconstruída. Nada dentro dela é tocado, que é a única posição defensável: assim que texto e marcação se entrelaçam, não sobra espaço em branco que se possa provar insignificante.
- Seções CDATA: delimitadores e conteúdo reproduzidos exatamente. Espaço ali dentro é conteúdo por definição.
- Valores de atributo: emitidos como foram escritos, com as aspas originais e quaisquer referências de entidade intactas.
- Texto num elemento folha: <name> Ada </name> perde só o preenchimento externo, nunca os caracteres do meio.
- Comentários: mantidos, a não ser que você peça o contrário. Instruções de processamento, declaração XML e subconjunto interno da DTD: sempre mantidos.
- xml:space="preserve": o limite honesto. Não recebe tratamento especial, então minifique uma cópia e compare se o seu documento usa isso.
Quanto menor, de verdade
Números de um documento de teste: 2.000 registros de pedido aninhados em quatro níveis. Com indentação de dois espaços ele tem 554 KB e minifica para 421 KB, 24 por cento a menos. Com quatro espaços tem 679 KB, então minificar tira 36 por cento. Documentos profundos com elementos pequenos ganham mais, porque a indentação é grande em relação ao conteúdo que envolve.
Agora comprima os dois. A versão de dois espaços vai para 30.744 bytes com gzip e a minificada para 29.771: três por cento de diferença. Com indentação de quatro espaços o resultado inverte, 28.519 bytes formatado contra 29.771 minificado, então o arquivo minificado é o maior dos dois depois da compressão. Indentação repetida é exatamente aquilo em que um compressor é bom. Minificar não é inútil; minificar para a rede é, assim que o Content-Encoding está ligado.
- Vale a pena: XML cru numa coluna de banco, uma mensagem de fila com limite de tamanho, um documento embutido em outro payload, uma fixture em que a indentação domina o diff.
- Não vale a pena: qualquer coisa servida por HTTP com compressão ligada, que é de onde o marketing da maioria dos minificadores tira a sua porcentagem.
- Ativamente prejudicial: um documento assinado. XML canônico mantém o espaço em branco no conteúdo dos elementos, então o resumo cobre bytes que você está prestes a mudar.
Recuperar o layout, e os limites
Passar a saída pelo formatador devolve um documento indentado com todos os valores idênticos. O que você não recupera é o seu layout original: linhas em branco entre seções, uma largura de indentação incomum, o espaçamento interno de uma subárvore mista em que duas tags ficavam lado a lado. Para conteúdo só de elementos, nada disso era informação. Se era, não minifique.
Um documento que não está bem formado não é minificado: sua entrada volta sem mudanças com todos os erros listados por linha e coluna. Um documento de 1 MB minifica em cerca de 180 milissegundos e 5 MB em torno de um segundo, num Web Worker para a página continuar respondendo. O teto é 20 MB, porque tudo fica na memória desta aba.
Minificando XML em código
Num passo de build isto é um parsing e uma reserialização, nunca uma operação de string. Cada exemplo faz o parsing de forma segura, já que os padrões de Java, PHP e da biblioteca padrão do Python resolvem entidades externas. Repare em como cada biblioteca decide qual espaço em branco é ignorável, porque essa decisão é a ferramenta inteira.
// Remove whitespace-only text nodes, but only where the element's children
// are all elements. Anything else is mixed content and belongs to the data.
function minifyXml(source) {
const doc = new DOMParser().parseFromString(source, 'application/xml');
if (doc.querySelector('parsererror')) {
throw new Error(doc.querySelector('parsererror').textContent.trim());
}
const strip = (el) => {
const kids = [...el.childNodes];
const elementOnly =
kids.some((n) => n.nodeType === 1) &&
kids.every((n) => n.nodeType !== 3 || !n.nodeValue.trim());
if (elementOnly) {
for (const n of kids) if (n.nodeType === 3) el.removeChild(n);
}
for (const child of el.children) strip(child);
};
strip(doc.documentElement);
return new XMLSerializer().serializeToString(doc);
}
// XMLSerializer emits <a/> for an empty element, so the output is not
// byte-identical to input that wrote <a></a>. Same document, different bytes.# lxml's remove_blank_text is the closest equivalent, and it is honest about
# being a guess: with no DTD it keeps blank text whose siblings carry real
# characters, which is what saves mixed content.
from lxml import etree
parser = etree.XMLParser(
remove_blank_text=True,
resolve_entities=False, # no entity expansion
no_network=True, # never fetch a DTD or an include
load_dtd=False,
huge_tree=False,
)
tree = etree.fromstring(source.encode('utf-8'), parser)
minified = etree.tostring(tree, encoding='unicode')
# Standard library, no dependency, same rule applied by hand:
#
# from defusedxml.ElementTree import fromstring
# root = fromstring(source)
# for el in root.iter():
# if len(el) and not (el.text or '').strip():
# el.text = None
# if not (el.tail or '').strip():
# el.tail = Noneimport javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;
import java.io.StringWriter;
import org.w3c.dom.*;
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setXIncludeAware(false);
Document doc = dbf.newDocumentBuilder().parse(new java.io.File("in.xml"));
// setIgnoringElementContentWhitespace only works when a DTD or schema tells
// the parser which content is element-only, which is usually not the case.
// So select the blank text nodes explicitly. The second predicate leaves
// mixed content alone: it skips blanks whose siblings hold real characters.
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList blanks = (NodeList) xpath.evaluate(
"//text()[not(normalize-space())][not(../text()[normalize-space()])]",
doc, XPathConstants.NODESET);
for (int i = 0; i < blanks.getLength(); i++) {
Node n = blanks.item(i);
n.getParentNode().removeChild(n);
}
TransformerFactory tf = TransformerFactory.newInstance();
tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
Transformer t = tf.newTransformer();
t.setOutputProperty(OutputKeys.INDENT, "no");
StringWriter out = new StringWriter();
t.transform(new DOMSource(doc), new StreamResult(out));
System.out.println(out);using System.Text;
using System.Xml;
// IgnoreWhitespace drops every whitespace-only text node the reader sees.
// Without a schema that includes the space between two inline elements in
// mixed content, so use this on element-only documents and set it false when
// the document carries prose.
var readerSettings = new XmlReaderSettings
{
IgnoreWhitespace = true,
DtdProcessing = DtdProcessing.Prohibit, // no external entities
XmlResolver = null,
MaxCharactersFromEntities = 1024 * 1024,
};
var writerSettings = new XmlWriterSettings
{
Indent = false,
NewLineHandling = NewLineHandling.None,
};
var output = new StringBuilder();
using (var reader = XmlReader.Create(new StringReader(source), readerSettings))
using (var writer = XmlWriter.Create(output, writerSettings))
{
writer.WriteNode(reader, defattr: true);
}
Console.WriteLine(output.ToString());<?php
$doc = new DOMDocument();
// preserveWhiteSpace must be false before the load, not after. libxml2 then
// applies its blank-node heuristic, which keeps blanks inside mixed content.
$doc->preserveWhiteSpace = false;
$doc->formatOutput = false;
libxml_use_internal_errors(true);
if (!$doc->loadXML($source, LIBXML_NONET)) {
foreach (libxml_get_errors() as $e) {
fprintf(STDERR, "XML error at line %d, column %d: %s\n",
$e->line, $e->column, trim($e->message));
}
libxml_clear_errors();
exit(1);
}
// saveXML() still ends the document with a newline; trim it if the byte
// count is what you are optimising.
echo rtrim($doc->saveXML());# --noblanks is libxml2's minifier. It removes ignorable whitespace only.
xmllint --noblanks --nonet document.xml > minified.xml
# xmllint has no flag for dropping comments, and sed cannot do it correctly
# (a comment may contain a > character). Use an identity transform with no
# template for comment() nodes:
cat > strip-comments.xsl <<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="no"/>
<xsl:template match="@*|node()">
<xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy>
</xsl:template>
<xsl:template match="comment()"/>
</xsl:stylesheet>
XSL
xsltproc --nonet strip-comments.xsl document.xml | xmllint --noblanks --nonet -
# Measure the two questions separately:
wc -c document.xml minified.xml
gzip -9 -c document.xml | wc -c
gzip -9 -c minified.xml | wc -cTodos estes fazem parsing e reserializam. Nenhum é uma expressão regular sobre sinais de menor e maior, porque a decisão que um minificador toma (este espaço está dentro de conteúdo só de elementos?) precisa da árvore. Uma ferramenta que promete minificar sem fazer parsing está avisando que vai errar no conteúdo misto.
Perguntas frequentes
Meu documento é enviado quando eu minifico?
Não. O analisador e o minificador rodam nesta aba como JavaScript num Web Worker. Não existe endpoint para onde postar, nem analytics com acesso ao editor, nem script de terceiros. Abra a aba Rede e veja ela continuar vazia enquanto você trabalha.
Isso não é detalhe aqui: documentos são minificados a caminho do armazenamento ou de outro sistema, o que significa que são payloads reais, com registros de pedidos, identificadores e tokens dentro. Várias ferramentas bem posicionadas nesta busca processam o seu documento num servidor, e uma das maiores publica documentos salvos por padrão.
Quanto o meu XML vai encolher?
Entre uns 10 e 35 por cento para XML indentado típico, determinado quase inteiramente pela profundidade do aninhamento e pela largura da indentação. Num documento de teste de 2.000 registros, a indentação de dois espaços caiu 24 por cento e a de quatro, 36. Documentos com muito texto ganham bem menos.
A comparação honesta é depois da compressão, e aí fica em torno de três por cento: 30.744 bytes com gzip formatado contra 29.771 com gzip minificado. Com indentação de quatro espaços, o arquivo formatado foi o menor dos dois. Julgue pelo destino do documento.
Minificar pode quebrar meu XML?
Pode, e é por isso que este aqui é conservador. O modo de falha é remover espaço em branco que era dado: conteúdo misto, CDATA e qualquer coisa governada por xml:space="preserve". Os dois primeiros estão tratados, já que subárvores mistas são copiadas literalmente da sua fonte e o CDATA é copiado byte a byte. Resta uma ressalva: xml:space="preserve" não recebe tratamento especial, então um elemento só de texto que o carregue ainda é aparado.
O terceiro modo de falha é invisível. Se o documento carrega uma assinatura XML, o XML canônico inclui o espaço em branco do conteúdo dos elementos no resumo, então minificar um documento assinado o invalida. Minifique antes de assinar, nunca depois.
Ele remove comentários?
Só se você pedir. A caixa vem desmarcada.
Comentários costumam ser a única documentação anexada a um arquivo de configuração. Removê-los de um POM do Maven ou de uma folha XSLT custa a explicação de por que um elemento estranho está ali, em troca de uma economia que o gzip teria recolhido de qualquer forma. Se o arquivo é seu e você minifica para armazenar, tire-os; se está repassando o documento de outra pessoa, deixe.
Dá para recuperar a versão formatada depois?
Dá, passando o documento minificado pelo formatador. Cada valor, atributo, referência de entidade e seção CDATA é idêntico, então nada do que um analisador reportaria mudou.
O que você não recupera é o seu layout original: linhas em branco entre seções, uma largura de indentação específica, ou o espaçamento interno de uma subárvore mista em que duas tags ficavam coladas. Para conteúdo só de elementos, nada disso carregava informação. Se carregava, guarde o arquivo original.
Que tamanho de arquivo ele aguenta?
Até 20 MB. Um documento de 1 MB minifica em aproximadamente 180 milissegundos e 5 MB em cerca de um segundo, num Web Worker para a página continuar respondendo.
O limite é de memória, não de política. Nada é enviado, então o documento vive nesta aba, e passando de 20 MB a árvore de análise pode ocupar várias centenas de megabytes e o navegador para de responder. Para arquivos maiores, o xmllint --noblanks faz o mesmo trabalho com a mesma regra sobre espaço ignorável.