Minifieur XML
Retire les espaces non significatifs. Le contenu mixte est épargné.
Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.
Collez un document ci-dessus et l’espace entre ses éléments est retiré. Le résultat revient sur une seule ligne à côté de votre saisie, avec la taille avant, la taille après et le pourcentage gagné. Les commentaires partent aussi si vous cochez la case. Tout tourne dans cet onglet : l’analyseur et le minifieur sont du JavaScript, et votre document n’est jamais envoyé nulle part.
Minifiez quand la contrainte est la taille du document lui-même. Des centaines de milliers de charges utiles dans une colonne de base de données, une enveloppe intégrée dans un champ texte JSON, un message qui doit tenir sous la limite d’un broker. Sur le réseau, cela vaut bien moins que ne le laissent croire les pourcentages annoncés, et les chiffres ci-dessous disent pourquoi.
La règle est celle du formateur, inversée : l’espace entre éléments est de la mise en page, l’espace à l’intérieur d’un contenu mixte est une donnée. Quand un élément contient à la fois du texte et des éléments enfants, son sous-arbre est repris de votre source au lieu d’être reconstruit, et le CDATA est copié octet pour octet. Ce qui reste ne peut pas changer ce qu’un analyseur remet à l’application.
Ce qu’il est sûr de retirer
Un analyseur XML remet à l’application chaque caractère du document, espaces compris ; il n’en écarte aucun pour vous. « Insignifiant » est une affirmation sur le modèle de contenu, pas sur l’analyseur : si une DTD ou un schéma déclarent qu’un élément a un contenu composé uniquement d’éléments, l’espace directement à l’intérieur ne peut pas être une donnée, et c’est le seul espace qu’un minifieur ait le droit de toucher.
La plupart des documents arrivent sans schéma, un minifieur applique donc une heuristique, et celle retenue ici est étroite : un nœud texte entièrement composé d’espaces, situé entre des éléments frères, est supprimé. Rien d’autre. C’est le jugement que porte libxml2 avec --noblanks. Un changement de plus : un élément vide est écrit auto-fermant, donc <status></status> devient <status/>. Tout analyseur conforme y voit le même élément, mais ce ne sont pas les mêmes octets.
Ce qui n’est pas retiré
Le contenu mixte est ce qui sépare un vrai minifieur d’une expression régulière. Dans <p>Bonjour <b>le monde</b> !</p>, l’espace après « Bonjour » est un caractère du document, et l’espace entre </b> et <i> dans <p>a <b>x</b> <i>y</i></p> aussi. Les deux survivent, parce qu’un sous-arbre mixte est copié tel quel depuis votre source au lieu d’être reconstruit. Rien à l’intérieur n’est touché, seule position défendable : dès que texte et balisage s’entremêlent, il ne reste plus d’espace dont on puisse prouver l’insignifiance.
- Sections CDATA : délimiteurs et contenu reproduits à l’identique. L’espace qui s’y trouve est du contenu par définition.
- Valeurs d’attribut : émises telles qu’écrites, avec le guillemet d’origine et les références d’entités intactes.
- Texte dans un élément feuille : <name> Ada </name> ne perd que le rembourrage extérieur, jamais les caractères entre les deux.
- Commentaires : conservés sauf demande contraire. Instructions de traitement, déclaration XML et sous-ensemble DTD interne : toujours conservés.
- xml:space="preserve" : la limite qu’il faut avouer. Aucun traitement particulier, donc minifiez une copie et comparez si votre document s’en sert.
Combien plus petit, réellement
Chiffres tirés d’un document de test : 2 000 enregistrements de commande imbriqués sur quatre niveaux. Avec une indentation de deux espaces, il fait 554 Ko et se minifie à 421 Ko, soit 24 % de moins. Avec quatre espaces il fait 679 Ko, la minification enlève donc 36 %. Les documents profonds à petits éléments gagnent le plus, car l’indentation est importante par rapport au contenu qu’elle entoure.
Compressez maintenant les deux. La version à deux espaces passe à 30 744 octets en gzip et la version minifiée à 29 771 : trois pour cent d’écart. Avec une indentation de quatre espaces le résultat s’inverse, 28 519 octets pour la version formatée contre 29 771 pour la minifiée : le fichier minifié est alors le plus gros des deux après compression. L’indentation répétée est exactement ce qu’un compresseur sait traiter. Minifier n’est pas inutile ; minifier pour le réseau l’est, dès que Content-Encoding est activé.
- Ça vaut le coup : du XML brut dans une colonne de base de données, un message de file à taille limitée, un document intégré dans une autre charge utile, une fixture où l’indentation domine le diff.
- Ça n’en vaut pas la peine : tout ce qui est servi en HTTP avec la compression activée, et c’est de là que la plupart des minifieurs tirent leur argumentaire chiffré.
- Franchement nuisible : un document signé. XML canonique conserve l’espace dans le contenu des éléments, donc l’empreinte couvre des octets que vous êtes sur le point de modifier.
Retrouver la mise en page, et les limites
Repasser la sortie dans le formateur vous redonne un document indenté dont chaque valeur est identique. Ce que vous ne retrouvez pas, c’est votre mise en page d’origine : lignes vides entre sections, largeur d’indentation inhabituelle, espacement interne d’un sous-arbre mixte où deux balises se touchaient. Pour un contenu composé uniquement d’éléments, rien de cela n’était de l’information. Si c’en était, ne minifiez pas.
Un document qui n’est pas bien formé n’est pas minifié : votre saisie revient inchangée avec toutes les erreurs listées par ligne et colonne. Un document de 1 Mo se minifie en 180 millisecondes environ et 5 Mo en une seconde à peu près, dans un Web Worker pour que la page reste réactive. Le plafond est de 20 Mo, parce que tout est gardé dans la mémoire de cet onglet.
Minifier du XML en code
Dans une étape de build, c’est une analyse suivie d’une resérialisation, jamais une opération sur des chaînes. Chaque exemple analyse de façon sûre, puisque les valeurs par défaut de Java, de PHP et de la bibliothèque standard de Python résolvent les entités externes. Observez comment chaque bibliothèque décide quel espace est ignorable : cette décision, c’est tout l’outil.
// 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 -cChacun de ces exemples analyse et resérialise. Aucun n’est une expression régulière sur des chevrons, car la décision que prend un minifieur (cet espace est-il dans un contenu composé uniquement d’éléments ?) réclame l’arbre. Un outil qui promet de minifier sans analyser vous annonce qu’il se trompera sur le contenu mixte.
Questions fréquentes
Mon document est-il envoyé quand je le minifie ?
Non. L’analyseur et le minifieur tournent dans cet onglet, en JavaScript, dans un Web Worker. Aucun point d’envoi, aucun analytics ayant accès à l’éditeur, aucun script tiers. Ouvrez l’onglet Réseau et regardez-le rester vide pendant que vous travaillez.
Ce n’est pas accessoire ici : on minifie un document sur le chemin du stockage ou d’un autre système, ce qui veut dire que ce sont de vraies charges utiles, avec des commandes, des identifiants et des jetons dedans. Plusieurs outils bien placés sur cette recherche traitent votre document sur un serveur, et l’un des plus gros publie par défaut les documents enregistrés.
De combien mon XML va-t-il rétrécir ?
Entre 10 et 35 % environ pour du XML indenté typique, ce qui dépend presque entièrement de la profondeur d’imbrication et de la largeur d’indentation. Sur un document de test de 2 000 enregistrements, deux espaces ont fait gagner 24 % et quatre espaces 36 %. Les documents riches en texte gagnent nettement moins.
La comparaison honnête se fait après compression, et là on est à trois pour cent : 30 744 octets gzippés pour la version formatée contre 29 771 pour la minifiée. Avec une indentation de quatre espaces, c’est la version formatée qui était la plus petite. Jugez en fonction de la destination du document.
La minification peut-elle casser mon XML ?
Elle le peut, et c’est pourquoi celle-ci est prudente. Le mode de panne consiste à retirer un espace qui était une donnée : contenu mixte, CDATA, et tout ce que régit xml:space="preserve". Les deux premiers sont traités, puisque les sous-arbres mixtes sont copiés tels quels depuis votre source et le CDATA octet pour octet. Une réserve demeure : xml:space="preserve" ne bénéficie d’aucun traitement particulier, un élément purement textuel qui le porte est donc quand même rogné.
Le troisième mode de panne est invisible. Si le document porte une signature XML, XML canonique inclut l’espace du contenu des éléments dans l’empreinte : minifier un document signé l’invalide. Minifiez avant de signer, jamais après.
Retire-t-il les commentaires ?
Seulement si vous le demandez. La case est décochée par défaut.
Les commentaires sont souvent la seule documentation attachée à un fichier de configuration. Les retirer d’un POM Maven ou d’une feuille XSLT vous coûte l’explication de la présence d’un élément étrange, pour un gain que gzip aurait ramassé de toute façon. Si le fichier est à vous et que vous minifiez pour le stockage, supprimez-les ; si vous transmettez le document de quelqu’un d’autre, laissez-les.
Puis-je récupérer la version formatée ensuite ?
Oui, en repassant le document minifié dans le formateur. Chaque valeur, attribut, référence d’entité et section CDATA est identique, donc rien de ce qu’un analyseur rapporterait n’a changé.
Ce que vous ne récupérez pas, c’est votre mise en page d’origine : lignes vides entre sections, largeur d’indentation particulière, ou espacement interne d’un sous-arbre mixte où deux balises se touchaient. Pour un contenu composé uniquement d’éléments, rien de cela ne portait d’information. Si c’était le cas, gardez le fichier d’origine.
Quelle taille de fichier accepte-t-il ?
Jusqu’à 20 Mo. Un document de 1 Mo se minifie en 180 millisecondes environ et 5 Mo en à peu près une seconde, dans un Web Worker pour que la page reste réactive.
Le plafond est une limite de mémoire, pas une politique. Rien n’étant envoyé, le document vit dans cet onglet, et au-delà de 20 Mo l’arbre d’analyse peut occuper plusieurs centaines de mégaoctets et le navigateur cesse de répondre. Pour les fichiers plus gros, xmllint --noblanks fait le même travail avec la même règle sur l’espace ignorable.