Minificador XML

Quita el espacio insignificante. El contenido mixto no se toca.

El espacio en blanco dentro de contenido mixto no se toca, porque ahí es contenido.
Entrada
Salida
En esperaPega un documento para comprobarlo. La validación se ejecuta mientras escribes.

Todo se ejecuta en esta pestaña. Nada de lo que pegues se sube, se registra ni se envía a ningún sitio. Abre el panel de red y compruébalo.

Pega un documento arriba y se elimina el espacio en blanco entre sus elementos. El resultado vuelve como una sola línea junto a tu entrada, con el tamaño antes, el tamaño después y el porcentaje ahorrado. Los comentarios también se van si marcas la casilla. Todo se ejecuta en esta pestaña: el analizador y el minificador son JavaScript, y tu documento no se envía nunca a ningún sitio.

Minifica cuando la restricción sea el tamaño del propio documento. Cientos de miles de payloads en una columna de base de datos, un envelope incrustado en un campo de texto JSON, un mensaje que tiene que caber en el límite de tamaño de un broker. Por la red vale mucho menos de lo que sugieren los porcentajes de titular, y los números de abajo explican por qué.

La regla es la del formateador, invertida: el espacio en blanco entre elementos es maquetación, el espacio dentro de contenido mixto es dato. Cuando un elemento contiene texto y elementos hijos a la vez, su subárbol se toma de tu fuente en lugar de reconstruirse, y el CDATA se copia byte a byte. Lo que queda no puede cambiar lo que un analizador le entrega a la aplicación.

Qué es seguro quitar

Un analizador XML entrega a la aplicación cada carácter del documento, espacio en blanco incluido; no hay ninguno que descarte en tu nombre. "Insignificante" es una afirmación sobre el modelo de contenido, no sobre el analizador: si una DTD o un esquema declaran que un elemento tiene contenido de solo elementos, el espacio directamente dentro de él no puede ser dato, y ese es el único espacio que un minificador puede tocar.

La mayoría de documentos llegan sin esquema, así que un minificador aplica una heurística, y la de aquí es estrecha: un nodo de texto compuesto enteramente de espacio en blanco, situado entre elementos hermanos, se descarta. Nada más. Ese es el mismo criterio que aplica libxml2 con --noblanks. Un cambio más: un elemento vacío se escribe autocerrado, así que <status></status> pasa a ser <status/>. Cualquier analizador conforme ve el mismo elemento, pero no son los mismos bytes.

Qué no se quita

El contenido mixto es lo que separa a un minificador de verdad de una expresión regular. En <p>Hola <b>mundo</b>!</p> el espacio tras "Hola" es un carácter del documento, y también lo es el espacio entre </b> y <i> en <p>a <b>x</b> <i>y</i></p>. Los dos sobreviven, porque un subárbol mixto se copia de tu fuente literalmente en vez de reconstruirse. Nada dentro de uno se toca, que es la única postura defendible: en cuanto texto y marcado se entrelazan, ya no queda espacio en blanco que se pueda demostrar insignificante.

  • Secciones CDATA: delimitadores y contenido reproducidos exactamente. El espacio ahí dentro es contenido por definición.
  • Valores de atributo: emitidos tal como estaban escritos, con el carácter de comilla original y cualquier referencia a entidades intacta.
  • Texto en un elemento hoja: <name> Ada </name> pierde solo el relleno exterior, nunca los caracteres de en medio.
  • Comentarios: se conservan salvo que pidas lo contrario. Instrucciones de procesamiento, declaración XML y subconjunto interno de la DTD: siempre se conservan.
  • xml:space="preserve": el límite honesto. No recibe trato especial, así que minifica una copia y compara si tu documento lo usa.

Cuánto más pequeño, en realidad

Números de un documento de prueba: 2.000 registros de pedido anidados a cuatro niveles. Con sangría de dos espacios ocupa 554 KB y se minifica a 421 KB, un 24 por ciento menos. Con cuatro espacios ocupa 679 KB, así que minificar quita un 36 por ciento. Los documentos profundos con elementos pequeños son los que más ganan, porque la sangría es grande en relación al contenido que envuelve.

Ahora comprime los dos. La versión de dos espacios pasa a 30.744 bytes con gzip y la minificada a 29.771: una diferencia del tres por ciento. Con sangría de cuatro espacios el resultado se invierte, 28.519 bytes formateado frente a 29.771 minificado, así que el archivo minificado es el mayor de los dos tras comprimir. La sangría repetida es justo en lo que un compresor es bueno. Minificar no es inútil; minificar para la red sí lo es, una vez que Content-Encoding está activo.

  • Merece la pena: XML en crudo en una columna de base de datos, un mensaje de cola con límite de tamaño, un documento incrustado en otro payload, un fixture donde la sangría domina el diff.
  • No merece la pena: cualquier cosa servida por HTTP con compresión activada, que es de donde sale el porcentaje del marketing de la mayoría de minificadores.
  • Activamente dañino: un documento firmado. XML canónico conserva el espacio en blanco del contenido de elementos, así que el resumen criptográfico cubre bytes que estás a punto de cambiar.

Recuperar la maquetación, y los límites

Pasar la salida por el formateador te devuelve un documento indentado con todos los valores idénticos. Lo que no recuperas es tu maquetación original: las líneas en blanco entre secciones, un ancho de sangría poco común, el espaciado interno de un subárbol mixto donde dos etiquetas estaban una al lado de la otra. Para contenido de solo elementos, nada de eso era información. Si lo era, no minifiques.

Un documento que no está bien formado no se minifica: tu entrada vuelve sin cambios con todos los errores listados por línea y columna. Un documento de 1 MB se minifica en unos 180 milisegundos y 5 MB en algo así como un segundo, en un Web Worker para que la página siga respondiendo. El techo son 20 MB, porque todo se mantiene en la memoria de esta pestaña.

Minificar XML desde código

En un paso de build esto es un análisis y una reserialización, nunca una operación de cadenas. Cada ejemplo analiza de forma segura, ya que los valores por defecto de Java, PHP y la biblioteca estándar de Python resuelven entidades externas. Fíjate en cómo decide cada biblioteca qué espacio en blanco es ignorable, porque esa decisión es la herramienta entera.

// 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 = None
import 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 -c

Todos estos analizan y reserializan. Ninguno es una expresión regular sobre corchetes angulares, porque la decisión que toma un minificador (¿está este espacio dentro de contenido de solo elementos?) necesita el árbol. Una herramienta que promete minificar sin analizar te está diciendo que se va a equivocar con el contenido mixto.

Preguntas frecuentes

¿Se sube mi documento cuando lo minifico?

No. El analizador y el minificador corren en esta pestaña como JavaScript en un Web Worker. No hay endpoint al que enviar nada, ni analítica con acceso al editor, ni scripts de terceros. Abre la pestaña Red y míralo seguir vacío mientras trabajas.

Aquí eso no es un detalle: los documentos se minifican de camino al almacenamiento o a otro sistema, lo que significa que son payloads reales con registros de pedidos, identificadores y tokens dentro. Varias herramientas posicionadas para esta búsqueda procesan tu documento en un servidor, y una de las más grandes publica por defecto los documentos guardados.

¿Cuánto se reducirá mi XML?

Entre un 10 y un 35 por ciento aproximadamente en XML indentado típico, determinado casi por completo por la profundidad del anidamiento y el ancho de la sangría. En un documento de prueba de 2.000 registros, la sangría de dos espacios bajó un 24 por ciento y la de cuatro un 36. Los documentos con mucho texto ganan bastante menos.

La comparación honesta es después de comprimir, y ahí ronda el tres por ciento: 30.744 bytes con gzip formateado frente a 29.771 con gzip minificado. Con sangría de cuatro espacios, el archivo formateado resultó el más pequeño de los dos. Júzgalo según a dónde va el documento.

¿Puede minificar romper mi XML?

Puede, y por eso este es conservador. El modo de fallo es quitar espacio en blanco que era dato: contenido mixto, CDATA y cualquier cosa gobernada por xml:space="preserve". Los dos primeros están cubiertos, ya que los subárboles mixtos se copian literalmente de tu fuente y el CDATA se copia byte a byte. Queda una advertencia: xml:space="preserve" no recibe trato especial, así que a un elemento de solo texto que lo lleve se le sigue recortando.

El tercer modo de fallo es invisible. Si el documento lleva una firma XML, el XML canónico incluye el espacio en blanco del contenido de elementos en el resumen, así que minificar un documento firmado lo invalida. Minifica antes de firmar, nunca después.

¿Quita los comentarios?

Solo si lo pides. La casilla está desactivada por defecto.

Los comentarios suelen ser la única documentación adjunta a un archivo de configuración. Quitarlos de un POM de Maven o de una hoja XSLT te cuesta la explicación de por qué hay ahí un elemento raro, a cambio de un ahorro que gzip habría recogido igualmente. Si el archivo es tuyo y minificas para almacenarlo, quítalos; si estás pasando adelante el documento de otra persona, déjalos.

¿Puedo recuperar después la versión formateada?

Sí, pasando el documento minificado por el formateador. Cada valor, atributo, referencia a entidad y sección CDATA es idéntico, así que nada de lo que un analizador reportaría ha cambiado.

Lo que no recuperas es tu maquetación original: líneas en blanco entre secciones, un ancho de sangría concreto, o el espaciado interno de un subárbol mixto donde dos etiquetas estaban pegadas. Para contenido de solo elementos, nada de eso llevaba información. Si la llevaba, conserva el archivo original.

¿Qué tamaño de archivo admite?

Hasta 20 MB. Un documento de 1 MB se minifica en unos 180 milisegundos y 5 MB en alrededor de un segundo, en un Web Worker para que la página siga respondiendo.

El tope es un límite de memoria, no una política. No se sube nada, así que el documento vive en esta pestaña, y pasados los 20 MB el árbol de análisis puede ocupar varios cientos de megabytes y el navegador deja de responder. Para archivos mayores, xmllint --noblanks hace el mismo trabajo con la misma regla sobre el espacio ignorable.

Herramientas relacionadas

Lecturas de referencia