使用方法
- 页面打开时输入框里已经放好一段 54 行的示例样式,原始体积 968 字节;结果区给出的就是它压缩后的样子,641 字节,省掉 327 字节、压缩率 33.8%。先看一眼这段,就知道输出长什么样。
- 换成自己的 CSS 时直接整段粘贴覆盖,不需要先手工格式化。这个 CSS压缩工具是逐字符扫描的,缩进乱七八糟、注释夹在中间、一行写十条声明都不影响结果。
- 点「压缩 CSS」即出结果;如果你更习惯键盘,在输入框里按 Ctrl + Enter(macOS 用 Cmd + Enter)同样触发——这里 Enter 必须留给换行,所以没有做成单键触发。
- 「复制结果」把压缩后的整段 CSS 放进剪贴板,「下载 .css」存成
style.min.css,可以直接替换线上的样式文件。 - 下面的「规则明细」表列出这次压缩命中了哪些规则、各命中多少次。想核对「哪一处空白被删了、哪一处是我故意留下的」,看这张表比肉眼比对代码快得多。
这个 CSS压缩工具能解决什么问题
一是上线前的最后一道手工。样式表在开发阶段总带着注释、缩进和分组空行,压缩后同一段示例从 968 字节变成 641 字节,而且这还只是起点:压缩后的文本再走 gzip,从 656 字节变成 457 字节,两条路叠加的收益比任何单一手段都大。
二是内联样式和关键 CSS的场景。把首屏样式塞进 HTML 的 <style> 标签里时,每一行缩进都在拖长首字节时间;工具把注释和缩进一次性剥干净,示例里内联块能小掉三分之一左右,正好用来换取更少的阻塞字节。
三是交接代码时先清一遍。别人发来的样式里常混着 /* stylelint-disable */、/* TODO */ 这类临时标记和整段被注释掉的旧规则,压缩一次就像做了一次「只删注释、不改语义」的清理,剩下的才是真正生效的代码。
四是把「哪里的空白不能删」这件事实验清楚。同一份输入可以反复改一处、再压缩一次,立刻看出这处的空白被保留还是被删掉,比翻规范快。规则明细表会把每次的命中次数列出来,做对照实验很直观。
本页不做这些事:不做值级优化(不把 0px 改写成 0、不把 #ffffff 缩成 #fff)、不合并重复选择器、不加浏览器前缀、不做语法校验、不生成 sourcemap。要先把样式排整齐再动手改,请用页面底部的「CSS格式化美化」;要检查括号有没有配对,请用「CSS校验」。
压缩原理:一个逐字符状态机与 17 条规则
这个 CSS压缩工具的核心不是一串正则替换,而是一次从左到右的单遍扫描,扫描过程中只有四个互斥状态:普通代码、字符串(单引号或双引号)、注释、url() 括号内部。每读到一个字符就按当前状态决定它的去向。之所以不用「先把字符串抠出来占位、再用正则删空白」这种常见做法,是因为正则无法区分「注释里的引号」和「字符串里的注释符号」——content: "/* 不是注释 */" 会被正则链清得面目全非,而 url(http://a/*b.png) 里的 /* 同样不是注释。
注释按「一个空白」处理,而不是直接删掉。这是最容易踩的一处:a/*x*/b 在 CSS 里是两个记号,直接把注释删成空串会得到 ab,从一个选择器列表变成了一个不存在的标签名。工具的做法是遇到注释就记下「此处有一个待定空格」,等读到下一个有效字符时再决定这个空格要不要落进结果——如果下一个字符是 {、}、; 这类会自己清空空白的标点,空格就丢掉。示例里的 3 段注释一共 150 字节,删完之后一个都不剩。
空白分两类:可以删的和不能删的。可以删的是标点直连处的空白——{ 与 } 两侧、; 两侧、, 两侧,以及括号外的 >、+、~ 两侧。不能删的是冒号前面的空白:.card :focus-visible 里的空格代表「后代元素」,删掉就变成同一个元素上的伪类 .card:focus-visible,两者匹配的节点完全不同。工具因此只删冒号之后的空白(color: #fff → color:#fff),冒号之前原样保留,代价是每处多留 1 个字节,换来的是不会改坏选择器。
括号里藏着一个必须保护的区域。扫描时工具维护一个括号深度计数器,只在深度为 0(也就是不在任何 () 里)时才动 >、+、~ 的空白。原因是 CSS Values and Units Level 3 明确规定 calc() 里的 + 和 - 必须前后都有空白:calc(100% - 8px) 合法,写成 calc(100%-8px) 整条声明直接失效。示例里的 calc(100% - 8px) 就是这样被完整保留的。
末尾分号能省,但有一个例外。块内最后一条声明后面紧跟 } 时,那个分号是纯冗余,工具会删掉;不过删之前会看一眼输出里分号前面那个字符是不是冒号——如果是,说明这是一条自定义属性的空值(--x: ;),分号必须留着,否则 --x: 会连声明都不成立。示例里 9 处末尾分号被删,而 --x: ; 这种写法被原样保住。
词法规则依据的是 W3C 的 CSS Syntax Module Level 3 里对 comment、string-token、url-token 的定义:注释以 */ 结束,字符串里的反斜杠可以转义下一个字符,url( 之后的内容在遇到右括号前整体当作一个记号。工具只处理「已经写好」的 CSS,不做语法解析,所以它不需要构造抽象语法树,单遍扫描的复杂度是 O(n),20 万字符的输入也能在一次点击内跑完。
完整示例(数值仅作演示)
示例条件与页面打开时的默认值完全一致:输入框里那段 54 行、968 字节的示例样式,其中包含 3 段注释、1 处 url()、1 处 calc()、若干带空白的字符串,以及一批多行缩进。压缩结果的实测数字是:
- 压缩后 641 字节(625 个字符),一共省下 327 字节,压缩率 33.8%;
- 其中注释贡献 3 处 / 150 字节,其余 177 字节来自空白、换行与可省的末尾分号;
- 这段文本再走 gzip:原始 656 字节 → 压缩后 457 字节,也就是先压缩再 gzip,比只 gzip 又少了 199 字节;
- 块内末尾分号删掉 9 处,冒号后的空白删掉 24 处,
>+~两侧空白删掉 8 处; - 反过来,保留的东西同样具体:字符串里 6 个空白字符、
url()里 2 个空白字符、calc()运算符两侧 2 处空白,以及散布在值列表与后代选择器里的 15 个单空格,一个都没动。
几个最容易看错的位置单独拎出来对照,左边是输入里的写法,右边是压缩后的结果,最后一列是规则依据:
| 输入片段 | 压缩后 | 依据 |
|---|---|---|
content: " 冒号 : 与空格 都保留 "; | content:" 冒号 : 与空格 都保留 " | 引号内整体是 string-token,空白与冒号都不参与规则判断 |
url( "icons/arrow right.svg" ) | url( "icons/arrow right.svg" ) | 括号内按不透明内容整体保留 |
calc(100% - 8px) center | calc(100% - 8px) center | 减号两侧的空白是语法要求,括号内的加减号一律不动 |
.card :focus-visible | .card :focus-visible | 后代选择器,冒号前的空格有语义 |
opacity: 0.5 !important | opacity:0.5!important | 冒号后的空白与感叹号前的空白都可省 |
对照输入与输出的第一行,还能看到一个细节:输入开头那段注释被删掉后,输出的第 1 个字符就是 .,前面不留空行、不留空格——工具在扫描结束后会把结果首尾残余的空白也一并裁掉,否则每次压缩都会莫名其妙多出 1 个字节。
输入范围与对照表
输入上限是 20 万字符(按输入框的 maxlength 计),够容纳一份中等项目的整包样式。下面这张表列出各类片段在压缩时的具体去向,遇到「为什么这处没被压掉」的疑问,先在这里对一眼:
| 输入片段 | 压缩后 | 依据 |
|---|---|---|
/* 说明 */ | 删除 | 注释按空白处理,两侧若紧邻记号则留一个空格 |
display: flex; | display:flex | 冒号后的空白可省 |
| 4 个空格缩进 + 换行 | 1 个空格 | 任意长度的空白串折叠成一个空格 |
.card > .title | .card>.title | 括号外的组合选择符两侧空白可省 |
.card ~ .tip | .card~.tip | 同上,+ 与 ~ 同样处理 |
.card :focus-visible | 原样 | 冒号前空白是后代选择器的一部分 |
a:hover | 原样 | 伪类本来就没有空格,不做任何增删 |
calc(100% - 8px) | 原样 | 加号减号两侧必须有空白 |
1fr 1fr | 原样 | 值列表里的单空格是分隔符,不能删也不能加 |
margin: 0 auto | margin:0 auto | 只删冒号后那一个空白 |
content: " x " | 原样 | 字符串内部整体保留 |
url( "a b.svg" ) | 原样 | 括号内整体保留 |
a{color:red;} | a{color:red} | 右花括号前那条分号可省 |
--x: ; | --x:; | 自定义属性空值,分号必须留 |
@charset "utf-8"; | 原样 | 后面不是右花括号,分号与引号都不动 |
另外三条与输入有关的具体限制:输入框里既没有 { 也没有 : 时,工具会判定「看起来不是 CSS」并报错,而不是返回一段被压得面目全非的文本;输入只有注释时压缩结果是空串,此时会提示你去补一条真实声明;输入超过 20 万字符会被拒绝,因为单次渲染这么大的结果会让输入框很难操作,拆成几段分别压缩更稳妥。
结果边界与字节口径
- 字符数不等于字节数。压缩率按 UTF-8 字节算,不是按
length算。示例里那 3 段注释共 150 字节,而它们只有 62 个字符——中文注释每个字 3 字节,所以「删注释省了多少」在带中文注释的样式表里比纯英文样式表更明显。 - 本页没有任何浮点运算,所以不存在舍入。唯一的「精度」问题是口径:同一段 CSS 按字符数是 625,按字节数是 641,两个数都对,看你拿它对比编辑器里的字符计数还是磁盘上的文件体积。
- 未闭合的注释与引号会被兜底处理。读到
/*却没找到配对的*/时,工具按「一直到文末都是注释」处理,并在提示区说明;字符串同理,会把文末当作字符串结束。这样你不会拿到半截被压坏的样式,但这类输入的结果只能当参考,正式文件请先修好语法。 - 压缩结果为 0 字节是一种有意义的报错。输入只有注释、或只有空白时,去注释去空白后什么都不剩,此时工具会明确报错,而不是静静给你一个空文件。
- 压缩会丢掉全部行号与列号。线上报错定位到
style.min.css:1:1842时基本没法反查,所以本页不生成 sourcemap——需要映射关系请保留一份未压缩的源文件,压缩产物只用于部署。 - gzip 之后再压缩,收益会收窄。示例数据是:只 gzip 得 656 字节,先压缩再 gzip 得 457 字节,等于又省 30.3%;但空白和缩进本身就是 gzip 最擅长压的内容,所以「压缩率 33.8%」与「gzip 后省 30.3%」这两件事有重叠,不能相乘当成省了 64%。
- 不做值级优化的原因很实在。
0px缩成0在大多数属性里没问题,但flex-basis: 0%与flex-basis: 0的弹性分配行为并不一致;#ffffff缩成#fff又要额外判断简写是否恰好等值。这些都属于「先解析、再回写」的改造,判断错就是线上样式错乱,因此本工具只做词法层面的安全动作。 - 老旧滤镜写法请绕开。形如
filter: progid:DXImageTransform...的历史遗留写法里带着冒号与括号,工具对冒号之后的空白一律删除,这类写法在语法上仍能解析,但如果你还在维护需要它的老项目,建议先用「CSS校验」把这类声明挑出来单独处理。
常见问题
为什么 content: " 冒号 : 与空格 都保留 " 里的空白一个都没删?
因为引号包起来的部分整体是 CSS 的 string-token:规范里字符串内部的所有字符(包括空格、冒号、花括号、注释符号)都只是普通字符。扫描器一旦进入引号状态就原样拷贝,直到遇到配对的引号为止,中间遇到反斜杠还会连带跳过下一个字符。示例里字符串内共 6 个空白字符,压缩后一个不少,输出的结果里能直接搜到 " 冒号 : 与空格 都保留 " 这一整段。
.card :focus-visible 中间那个空格为什么必须留?
那是后代选择器,匹配 .card 内部的任意后代元素。把空格删掉变成 .card:focus-visible,含义就成了「.card 自己获得焦点」,匹配范围完全不同。所以工具只删冒号后面的空白,冒号前面的空白一律保留——这也是它和很多「无脑正则删空格」脚本最关键的区别。
calc(100% - 8px) 里的空格能删吗?
不能。CSS Values and Units Level 3 规定 calc() 里 + 与 - 两侧必须各有一个空白字符,写成 calc(100%-8px) 时整条声明会被当作语法错误丢弃,属性退回上一个有效值或默认值。工具在括号深度大于 0 时不碰 + - > ~,所以示例里的这处写法原样出现在结果里。
url() 括号内的空白为什么也整体保留?
一是括号内通常是引号字符串,本来就属于字符串状态;二是即使写成不带引号的 url( icons/a b.svg ),工具也把它整段当不透明内容。原因很实际:url() 里可能出现 /*、;、} 这些在别处有语法含义的字符,例如 url(http://a/*b.png),一旦按通用规则去删空白、去注释,URL 就会被截断。为了两个字节冒这个风险不值得。
最后一条声明的分号为什么可以省,--x: ; 又为什么不能?
CSS 的声明列表里,分号是「分隔符」而不是「结束符」,最后一条后面紧跟 } 时不需要分隔,所以能删。但自定义属性 --x: ; 是把「一个空白」当作有效值,分号一删,剩下的 --x: 会连声明结构都不完整,取值时行为可能和原来不同,所以工具在删分号前会检查分号左侧是不是冒号,是就保留。
注释为什么是「变成一个空格」而不是直接删掉?
因为注释在词法上等价于空白。写 a/*x*/b 时两个标识符之间必须有分隔,直接删注释会得到 ab,把两个记号粘成了一个。同理 margin:0/*居左*/auto 也不能删成 margin:0auto。工具把注释记成「待定空白」,再由下一个字符决定这个空白是丢弃(后面是 { } ; , 这类标点时)还是真的落进结果。
压缩完还能再 gzip / br 吗?大概能省多少?
可以,两者不冲突,而且应该一起用。示例实测:原始 968 字节单独 gzip 得 656 字节;压缩后的 641 字节再 gzip 得 457 字节,比只 gzip 又少了 199 字节。服务端如果开启 br(Brotli),一般还能再低 10%~15%,因为 Brotli 自带一份常用 CSS 属性的静态字典,对 display、position 这类高频词特别有效。
为什么不做 0px → 0、#ffffff → #fff 这类「更狠」的优化?
因为它们都需要先解析再回写,而语义变化的风险很高:flex-basis: 0% 与 flex-basis: 0 在弹性分配上的表现并不同;颜色简写还要判断是否与六位写法恰好等值。CSS压缩工具的定位是「只删掉确定没有语义的部分」,每一条规则都能用一句话说清依据;想把值也一起优化,建议交给构建期的 PostCSS 或 cssnano,这样改完还有测试兜底。