雜記 › 技術筆記 › CSS 壓縮沒吃掉 calc 的空格,美化卻把 .btn:hover 劈成了兩截

CSS 壓縮沒吃掉 calc 的空格,美化卻把 .btn:hover 劈成了兩截

一、排完版上線,懸停態集體失效

壓了好幾年沒人動過的樣式表,接手後的第一件事通常是排一遍版。243 字元的輸入美化出來是 291 字元、26 行,每條屬性獨佔一行,看著比原來舒服。上線當天回饋就到:按鈕沒有懸停態,列表項的前綴圖示也不見了。

逐行比對輸出,根因落在 .btn: hover。原始檔案寫的是 .btn:hover,冒號後憑空多出一個空格,選擇器當場失配;同一段的 li::before 出來是 li: : before。只把 .btn:hover{opacity:0.8}li::before{content:""}a:focus{outline:none} 丟進 CSS 美化壓縮,66 字元變成 90 字元、12 行,三個偽類沒有一個倖免——那頁首屏預填的樣例本來就帶著這條偽類,點一次美化就能復現。

二、美化只認字元,不認冒號的身份

CSS 裡的冒號至少有四種身份:偽類與偽元素、屬性與值之間、媒體特徵裡、字串內容裡。美化對四種一視同仁,看見冒號就補一個空格:補在屬性上是可讀性,補在媒體條件裡碰巧仍然合法,補在偽類上是語法事故,補在引號裡直接改了內容。69 字元的 @media (max-width:600px){.a{color:red}.b{padding:calc(100% - 4px) 0}} 美化成 87 字元、11 行,條件冒號被補上空格、左花括號後緊跟著空行。

JS 美化壓縮 那篇的破壞點在分號與換行,識別符號與冒號之間沒有語義綁定;CSS 的偽類偏偏要靠這個緊貼的冒號活著。

美化前 66 字元 美化後 90 字元 12 行 .btn:hover 變 .btn: hover li::before 變 li: : before 補出的空格讓整條規則失效

圖 1:偽類樣例美化前後的字元與行數

三、縮排那一欄,三檔輸出逐位元組相同

沒有縮排,第一個反應是去動「縮排方式」,那裡確實擺著 2 空格、4 空格、Tab 三檔。可同一段樣例在三檔下都是 291 字元、26 行、每行頂格在 0 列,逐位元組沒有差別:字元迴圈裡算過縮排,隨後一次逐行 trim 全抹掉,收尾再用恆為 0 的層級終值補回去。切到壓縮檔時這一欄只是變淡(實測 opacity 0.5),下拉選單也沒有被停用,改完照樣什麼都不發生。

四、真正保守的是壓縮:calc 的空格它沒動

兩樁先入為主都不成立。一是 calc():.box{width:calc(100% - 20px);padding:calc( 10px - 5px )} 60 字元壓到 56 字元,減號兩側的空格一個沒丟,多寫的會被歸一;反方向也不替你補,無效的 calc(100%-20px) 27 字元壓完一字未改。

二是 IE hack:.ie6{*zoom:1;_height:1%;width:10px} 35 字元壓完原樣,前導的 * 與 _ 沒被當成非法宣告吞掉。!important 同理,red !important 與 none!important 原樣混排出來,47 字元壓完仍是 47 字元。

「去除所有空白」同樣不成立:壓縮只刪花括號、冒號、逗號鄰邊的空白與換行,唯一改動可見內容的是字串,21 字元的 .s{content:"x, y; z"} 壓成 19 字元。

五、/*! */ 沒有特權,去留只在那一個勾選上

打包工具普遍把 /*! */ 當作保留註解,這一版沒有特例:它與普通註解走同一個比對分支,去留只由那個預設勾選決定。/*! keep me: v1.0; license */ 配一條普通註解和 body{color:red} 共 64 字元,取消勾選後壓到 15 字元、少了 76.6%,授權標頭跟著消失;勾著壓是 61 字元、只降 4.7%,註解內部照樣被改寫,keep me: v1.0 成了 keep me:v1.0。這跟 SQL 美化 那篇的「註解去哪了」是同一類口徑:勾選框只決定有無。

六、字串和 url() 裡的分號,被當成宣告分隔符

美化的第二個破壞點在最不該斷的地方。56 字元的 .i{background:url("data:image/png;base64,iVBORw0KGgo=")} 美化成 65 字元:base64 那串在分號處被從中斷開換行,data: 後面也被補出空格。那 49 字元的通訊協定位址也不放過,出來是 url(https: //www.uutools.com/a.png)。內嵌圖示經過美化,不是排版變差,是整筆資源被丟掉。

七、美化不冪等,壓縮冪等

貼回輸出再跑一遍,字元數還會漲:預設樣例第一遍 291、第二遍 310,多出的 19 正是輸出裡的冒號數,第三遍仍是 310;calc 例子 64 到 67 再 67,@media 例子 87 到 93 再 93。壓縮的四組樣例首跑就穩定,243 字元連壓三遍都是 243。

縮排 2 空格 291 字元 縮排 4 空格 291 字元 縮排 Tab 291 字元 美化第 2 次 310 字元 美化第 3 次 310 字元 三檔縮排輸出一條線,全部頂格 壓縮三遍都停在 243 字元

圖 2:縮排三檔與美化兩次迭代的輸出

八、體積收益要另算,統計條也不是位元組表

2000 條規則、91780 字元的樣式表壓完是 89780 字元,只降 2.2%、耗時 0.8 ms;同一份美化到 107779 字元、10000 行,漲 17.4%、耗時 211.6 ms。輸入本就沒什麼多餘空白時,壓縮檔就只有這個量級。

統計條還要換算口徑:數的是 UTF-16 碼元不是位元組,含中文的樣例報 25 字元、UTF-8 實際 33 位元組。美化檔的「變化」是淨增,那 243 字元顯示 +19.8% 並標紅,零變化寫成 +0%,異常時結果 0 字元會渲染成綠色的 -100.0%。

現象先查哪一處實測數字
懸停態失效選擇器冒號後多出的空格66 字元變 90 字元
縮排改了沒反應三檔是否真的改變輸出291 字元、26 行、頂格
內嵌圖示消失data URI 的分號是否被斷行56 字元撐成 65 字元
calc 減法報錯減號空格原本寫沒寫60 壓到 56、27 一字未改

Q:怎麼第一時間找出被劈開的偽類?

盯住冒號後面那個多出來的空格:.btn:hover 變 .btn: hover、li::before 變 li: : before。這一版不檢驗合法性,非法輸入照樣走完美化也不提示,行數從 1 漲到 26 不等於結果可用。

Q:壓縮會把 calc 的空格吃掉嗎?

不會:60 字元壓到 56 字元,calc(100% - 20px) 原樣;它也不替你補,無效的 calc(100%-20px) 壓完仍是 27 字元。IE hack 與 !important 的空格同樣不動,會出事的是美化檔。

Q:想要 4 空格縮排,這一頁給得了嗎?

給不了:三檔對同一段樣例都輸出 291 字元、26 行且全部頂格。這一檔只用來看結構,上線前按偽類與 data URI 這兩處自查。

📎 相關文章

文本合併後行數對不上:缺尾末換行、被吃掉的 BOM 和不可靠的順序 2026-10-26 Excel 貼上轉 JSON:日期、長數字與精度在貼上那一刻就已經變了 2026-10-26 條碼解碼只吐原文:EAN-13 檢查碼、UPC-A 的前導零與 CODE128 的模 103 2026-10-26 二維碼解碼實測:糾錯等級救不了遮擋,每模組 2 像素才是生死線 2026-10-26 字頻還是詞頻:同一句中文,一份 23 項榜單和一份空表 2026-10-26