Notes › Tech Notes › HTML Beautify and Minify Are Not Inverses: A Round-Trip Byte Audit of 10 Samples

HTML Beautify and Minify Are Not Inverses: A Round-Trip Byte Audit of 10 Samples

Ten HTML samples compared byte by byte after a round trip all differ: beautify of minify equals beautify in all ten, while minify of beautify matches in none. 128 to 216 to 134 bytes explains the leftover spaces, pre stays protected only when paired, and the counter reports UTF-16 characters, not bytes.

A one-line email template started the question

A colleague in operations forwarded an EDM template and said the diff had turned completely red after she changed one line of copy and committed it. Opening the file, I found the whole HTML document squeezed onto a single line by some build script, with no line break and no indentation left between the tags. I pasted that line into the panel and pressed beautify. The indentation and the line count came back, and the diff stayed red, because every text node now carried extra spaces around it. That raised the question: are beautify and minify inverse operations, and does one round trip return the original bytes? Ten samples of different structure, compared byte by byte, answer it: invertibility holds in only one direction.

20 byte-by-byte comparisons and an asymmetric failure

The ten samples differ on purpose: a pure ASCII pre-filled string, a table carrying Chinese text, a paired pre block, a textarea, an inline script, uppercase tags in single quotes, inline text with spaces, a run of void tags, three nested Chinese layers, and a page mixing in a style element. Each one goes through minify(beautify(x)) and beautify(minify(x)) separately, and every result is compared with the original string byte for byte. Twenty comparisons, zero matches. The failure is not symmetric. beautify(minify(x)) equals beautify(x) for all ten samples, so compressing first and then beautifying produces exactly what beautifying alone produces; minify(beautify(x)) equals minify(x) for none of the ten. The UTF-8 byte account reads as follows.

SampleRaw BBeautify BMinify Bmb Bbm B
S1 pre-filled single line128216128134216
S2 Chinese table7296545896
S3 pre block2532233132
S4 textarea5269526669
S5 inline script9311093107110
S6 uppercase plus comment6074575974
S7 inline text5269525669
S8 void tags4056394556
S9 three nested layers1161498589149
S10 with style10215298120152

The bm column agrees with the beautify column on every row, while the mb column agrees with neither the minify column nor the raw string on any row. Minify looks straight through beautify, and beautify cannot undo minify.

128 to 216 to 134: where the extra bytes come from

The pre-filled string is 128 bytes of pure ASCII. Pressing beautify in the HTML beautify and minify panel turns it into 216 bytes, and every added byte is leading indentation or a line break. Compressing that result back gives 134 bytes rather than 128, six bytes over. Searching the surplus at offset 34 finds <title> Test </title>: beautify pushes a text node onto its own line and trims it, leaving one space on each side, and the minify pattern >\s+< only matches a boundary made of tags alone, so the spaces flanking a text line fall outside its coverage. The three text nodes Test, Hello and World contribute 2 bytes apiece, and that totals exactly six.

pre is protected only when it comes in pairs

The preserve mechanism inside minifyHTML depends on a pattern that matches an opening tag and a closing tag together. The paired sample "<div> x <pre>L1 L2</pre> y </div>" enters at 40 bytes and leaves at 40 bytes with the whitespace inside the pre body untouched, and uppercase PRE reaches the same branch. Left unclosed, "<pre>keep me please" collapses from 24 bytes to 19 bytes, because the generic whitespace pattern swallows every space and line break inside it. Protection is conditional: a pair enters the preserve pool and everything else degrades.

The counter reports characters, not bytes

updateStat reads the .length property of a JavaScript string, so the number on screen is a count of UTF-16 code units, not a count of UTF-8 bytes. The Chinese sample displays 11 characters while its UTF-8 encoding really takes 19 bytes; a sample holding a surrogate pair displays 10 characters and occupies 14 bytes. The ratio line uses the same character basis, so the pre-filled first screen moving from 128 characters to 216 characters is drawn as a red +68.8%. A transfer size estimate has to re-encode the text in UTF-8, since the counter value cannot stand in for that result.

S1 round trip in bytes 128 B 216 B 134 B → → raw 128 bytes beautify +88 re-minify +6 bm==b holds for all ten mb==m matches none

Figure 1: the byte flow of one round trip, and the two asymmetric statistics beside it.

Idempotence splits, and the placeholder can be injected

Beautify is not idempotent on a string holding a paired pre block: a source of 55 characters grows round after round to 83, then 96, then 104 characters. The first pass moves the pre block onto its own lines, and the second pass treats the line breaks inside the pre body as original content and stacks another layer of indentation on them. A string without a preserved block stays idempotent and returns 102 characters on both passes. Running minify a second time reproduces the first output byte for byte, with or without a preserved block.

The internal placeholder protocol ___PRESERVE_n___ that minify uses to park preserved chunks can also be injected by a literal written in the body text. The input "<div>___PRESERVE_0___</div><!--C C-->" measures 37 bytes, and its output "<div><!--C C--></div><!--C C-->" measures 31 bytes: the placeholder in the body was replaced by preserved[0], one comment, so that comment now appears twice. An out-of-range index such as ___PRESERVE_5___ prints the literal undefined.

findTagEnd understands quotes, and it can leak

BeautifyHTML uses findTagEnd(code, start) to locate the end of a tag. Measured on an attribute value containing >, the scan closes at character 13, which confirms that a > inside quotes does not truncate the tag. With double quotes crossing into single quotes, the sample "<div t="it's" u='q"r'>x" closes at character 22. When a quote leaks and never closes, the scan keeps going and swallows everything up to character 17, the end of the string. An unclosed tag raises no complaint; it only deepens the following indentation abnormally.

findTagEnd close points gt in quotes closes: 13 crossed quotes: 22 leaked quote to end: 17 unclosed: no error paired pre 40B in 40B out 24B→19B broken

Figure 2: the three closing behaviours of findTagEnd, side by side.

The comment checkbox and the invariants that survive

One input holding a comment measures 44 bytes. With the keep-comments box checked the output is 43 bytes and the comment survives together with the whitespace inside it; unchecked, the output drops to 19 bytes and the comment disappears whole. Attribute quoting and tag case stay byte for byte identical under both settings, so a single quote remains a single quote, a double quote remains a double quote, and an uppercase DIV is never rewritten as a lowercase div.

Indentation width is a display-layer idea

getIndent offers three settings, two spaces, four spaces and a tab, each producing a different number of leading characters. None of that survives compression, because the minify pattern >\s+< deletes the leading run whatever it contains. Switching the setting changes nothing in the minify output, so indentation width never appears as a term in the byte account.

Frequently asked questions

Q: Does beautify followed by minify give the result of minify alone?

No. minify(beautify(x)) matches the raw string byte for byte in none of the ten samples, because the spaces beautify leaves around a text line, and the indentation it leaves inside a preserved block, are exactly the residue minify cannot delete. The opposite order holds: beautify(minify(x)) equals beautify(x) for all ten.

Q: Does an unclosed pre tag raise an error?

No. An unclosed block fails the paired pattern, never enters the preserve pool, and has its whitespace folded by the generic rule; beautify only deepens the indentation and throws nothing. For the wider set of cases, the earlier article HTML beautify and minify notes covers that ground.

Q: Can the numbers in the counter be used as byte counts?

No. updateStat reads UTF-16 code units, so 11 characters may really occupy 19 bytes once encoded. Recompute the ledger in UTF-8.

Q: What happens if my body text contains ___PRESERVE_0___?

It is treated as a protocol placeholder and restored as entry zero of the preserve pool, which duplicates whatever that entry holds. Ordinary HTML never meets the condition.

📎 Related articles

How a 1 GB File Is Built From 1024 Chunks: Chunk Rhythm, Cancel Boundaries and Three Fill Bytes in a File Generator 2026-11-04 Three Edges of Filename Extraction: The Trailing Slash Backstops One Step, the Query String Stays Whole, Blank Lines Vanish Silently 2026-11-04 Turn encodeAll On and Chinese First Becomes &amp;#20013;: HTML Entity Table Coverage and Decode Tolerance, Measured 2026-11-04 You Paste 100 Fraction Rows and Row 45 Quietly Becomes 1/4: Who Owns the Minus Sign, Blank Lines and Big Numbers on This Page 2026-11-03 Paste 3. Start Into Markdown to HTML: Why Ordered List Numbering Only Honours the First Number 2026-11-03