杂记 › 技术笔记 › HTML 美化与压缩不是互逆:10 个样本的往返字节账实测

HTML 美化与压缩不是互逆:10 个样本的往返字节账实测

十份HTML样本往返逐字节对照全不等;beautify∘minify=beautify十条恒中反向全不中。128→216→134解释残余空格,pre成对才保护,统计条报UTF-16字符非字节——失败不对称。

一封被压成单行的邮件模板引发的疑问

场景很日常:运营同事转来一份 EDM 模板,说"在编辑器里改了个文案,提交后 diff 全红了"。打开一看,整段 HTML 被某个构建脚本压成了一行——标签之间无换行无缩进。我顺手把这一行粘进工具点了"美化",缩进和行数回来了,但 diff 仍然全红:文本节点周围多出若干空格。这让我想验证一个根本问题:美化与压缩是不是互逆操作?往返一次能否回到原始字节?十份不同结构的样本逐字节对照给出答案——互逆性只在一个方向成立。

20 组逐字节对照与不对称失败

十份结构差异明显的样本——纯 ASCII 预填串、含中文表格、成对 pre 块、textarea、内联 script、大写标签加单引号、行内文本空格、void 标签串、三层嵌套中文、混合 style——每份分别跑 minify(beautify(x)) 与 beautify(minify(x)),和原串逐字节比较。二十组对照,零组相等。但失败不对称:beautify(minify(x)) 等于 beautify(x) 十条全中,即先压后美化输出和直接美化完全相同;minify(beautify(x)) 等于 minify(x) 十条全不中。下面给 UTF-8 字节对照:

样本原串 B美化 B压缩 Bmb Bbm B
S1 预填一行式128216128134216
S2 表格中文7296545896
S3 pre 块2532233132
S4 textarea5269526669
S5 内联 script9311093107110
S6 大写加注释6074575974
S7 行内文本5269525669
S8 void 标签4056394556
S9 三层嵌套1161498589149
S10 含 style10215298120152

bm 列每行与美化列一致,mb 列与压缩列或原串无一相等——minify 对 beautify 透明,beautify 对 minify 不可逆。

128、216、134:多余字节从哪来

预填串 128 字节纯 ASCII,在HTML 美化压缩面板点"美化"后变 216 字节——增量全来自行首缩进和换行符。再"压缩"回去得到 134 字节而非 128:多出 6 字节。定位偏移 34 发现 <title> Test </title>——beautify 把文本节点下推独立行并 trim,两侧各留一空格;minify 的 >\s+< 正则只匹配纯标签边界,文本独立行两侧的空格不在覆盖范围。Test、Hello、World 三个文本各贡献 2 字节,合计恰好 6。

pre 成对才受保护,未闭合即破防

minifyHTML 的保留机制依赖正则匹配成对开闭标签。实测成对样本 "<div> x <pre>L1 L2</pre> y </div>" 40 字节进 40 字节出,pre 内部空白原样不动;大写 PRE 同样命中。未闭合时 "<pre>keep me please" 24 字节被折叠成 19 字节——内部空格和换行全被通用正则吞掉。保护是条件性的:成对才进保留池,未闭合则退化。

统计条报的是字符,不是字节

updateStat 取的是 JS 字符串 .length——即 UTF-16 码元数,不是 UTF-8 字节数。中文样本显示 11 字符,实际 UTF-8 编码 19 字节;含代理对的样本显示 10 字符,实占 14 字节。比率也基于字符口径:预填首屏从 128 字符变 216 字符,变化 +68.8% 红字。做传输体积估算必须用 UTF-8 编码器重算,不能引用统计条数值。

S1 预填串往返字节流 128 B 216 B 134 B → → 原始 128 字节 美化 +88 字节 再压回 +6 残余 bm==b 十条全中 mb==m 十条全不中

图 1:S1 往返字节流向与不对称统计

幂等性分裂与占位符注入

beautify 对含成对 pre 的串不幂等:55 字符源逐轮变长 83→96→104 字符——b1 把 pre 拆成独立行后,b2 把 pre 体内换行行首当作原内容再叠缩进。不含保留块的串幂等:两次均 102 字符。minify 二次运行逐字节相同,无论含不含保留块。

minify 内部占位协议 ___PRESERVE_n___ 可被正文字面量注入。输入 "<div>___PRESERVE_0___</div><!--C C-->" 37 字节,输出 31 字节 "<div><!--C C--></div><!--C C-->"——正文占位符被 preserved[0](一条注释)替换,该注释出现两次。越界编号 ___PRESERVE_5___ 输出字面量 undefined。

findTagEnd 的引号感知与泄漏

beautifyHTML 用 findTagEnd(code, start) 定位标签结束。实测属性值含 > 时在第 13 字符收口——引号内 > 不截断;双引号套单引号交叉 "<div t="it's" u='q"r'>x" 在第 22 字符收口;引号泄漏(开引号未闭合)则一路吞到第 17 字符串尾。未闭合不报错,只让缩进异常加深。

findTagEnd 收口位置 引号内 > 正常截断: 13 双套单交叉截断: 22 引号泄漏吞到串尾: 17 未闭合标签不抛异常,缩进只增不减 成对 pre 40B 进 40B 出 24B→19B 破防

图 2:findTagEnd 三种收口行为对照

注释复选框与格式不变量

同一含注释输入 44 字节,勾选保留时输出 43 字节——注释连同内部空白完整留下;取消勾选输出 19 字节——注释整段删除。属性引号与标签大小写两档下逐字节不变,单引号仍单引号、双引号仍双引号、DIV 不转 div。

缩进宽度是显示层概念

getIndent 三档(2 空格、4 空格、Tab)给不同前导字符数,但 minify 的 >\s+< 正则把这些全部删除——换档不影响 minify 输出,字节账里不出现缩进宽度变量。

FAQ

Q:美化再压缩能否得到直接压缩的结果?

不能。minify(beautify(x)) 与原串逐字节十条全不中——beautify 留下的文本独立行空格和保留块缩进是 minify 删不掉的残余。反过来 beautify(minify(x)) 等于 beautify(x) 十条全中。

Q:未闭合 pre 会不会报错?

不会。未闭合不满足成对正则,不进保留池,内部空白被通用正则折叠;beautify 也只加深缩进不抛异常。更多场景选择可参考旧文 HTML 美化压缩说明。

Q:统计条的数字能当字节数用吗?

不能。updateStat 取 UTF-16 码元数,11 字符可能实占 19 字节。字节账必须另算 UTF-8。

Q:正文写了 ___PRESERVE_0___ 会怎样?

被当成占位符还原成保留池第 0 号内容,导致注释重复。正常 HTML 不会触发这条件。

📎 相关文章

1 GB 文件由 1024 块拼成:任意大小文件生成的分块节奏、取消边界与三种填充字节 2026-11-04 文件名提取的三处边界:结尾斜杠只兜一格、查询串整块留下、空行静默消失 2026-11-04 encodeAll 一开,中文先变成 &amp;#20013;:HTML 实体表覆盖与解码容错实测 2026-11-04 粘贴 100 行分数,第 45 行被悄悄算成 1/4:约分这页的负号、空行和大数各归谁管 2026-11-03 把 3. 起步贴进 Markdown 转 HTML,为什么有序列表的编号只认第一个数 2026-11-03