CSS压缩工具

逐字符扫描的 CSS 安全压缩:删掉注释、把缩进和换行折叠成一个空格、去掉标点周围与块内末尾的分号,但字符串、url() 与 calc() 里的空白一个都不动。

原始968 字节
压缩后641 字节
节省327 字节
压缩率33.8%

规则明细(上面这段示例的命中情况)

规则命中说明
移除 /* */ 注释3 处按一个空白处理,避免 a/*x*/b 被粘成 ab
折叠连续空白34 处缩进、换行、制表符统一压成一个空格
删除 { 前的空白10 处选择器与左花括号之间不需要空白
删除 { 后的空白10 处左花括号后立刻开始声明
删除 } 前的空白10 处块尾的空白一律去掉
删除 } 后的空白8 处右花括号直接接下一个选择器
删除 ; 前的空白0 处这段输入里的分号前空白,没有就是 0
删除 ; 后的空白14 处分号后直接接下一条声明
去掉块内末尾声明的分号9 处右花括号前那条分号可省,--x:; 例外
删除 , 两侧的空白1 处选择器分组与值列表都用逗号分隔
删除 > + ~ 两侧空白8 处只在括号外生效,calc() 里的运算符不动
删除 : 后的空白24 处属性名后的冒号;冒号前的空白保留

提示:上表先显示 12 行,点「加载更多」展开全部 17 条;「复制对照表」复制制表符分隔文本,「导出 CSV」带 UTF-8 BOM,Excel 打开不乱码。表里的数字就是上面这段示例 CSS 的真实命中次数,换一段输入会整套重算。

使用方法

  1. 页面打开时输入框里已经放好一段 54 行的示例样式,原始体积 968 字节;结果区给出的就是它压缩后的样子,641 字节,省掉 327 字节、压缩率 33.8%。先看一眼这段,就知道输出长什么样。
  2. 换成自己的 CSS 时直接整段粘贴覆盖,不需要先手工格式化。这个 CSS压缩工具是逐字符扫描的,缩进乱七八糟、注释夹在中间、一行写十条声明都不影响结果。
  3. 点「压缩 CSS」即出结果;如果你更习惯键盘,在输入框里按 Ctrl + Enter(macOS 用 Cmd + Enter)同样触发——这里 Enter 必须留给换行,所以没有做成单键触发。
  4. 「复制结果」把压缩后的整段 CSS 放进剪贴板,「下载 .css」存成 style.min.css,可以直接替换线上的样式文件。
  5. 下面的「规则明细」表列出这次压缩命中了哪些规则、各命中多少次。想核对「哪一处空白被删了、哪一处是我故意留下的」,看这张表比肉眼比对代码快得多。

这个 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()、若干带空白的字符串,以及一批多行缩进。压缩结果的实测数字是:

几个最容易看错的位置单独拎出来对照,左边是输入里的写法,右边是压缩后的结果,最后一列是规则依据:

输入片段压缩后依据
content: " 冒号 : 与空格 都保留 ";content:" 冒号 : 与空格 都保留 "引号内整体是 string-token,空白与冒号都不参与规则判断
url( "icons/arrow right.svg" )url( "icons/arrow right.svg" )括号内按不透明内容整体保留
calc(100% - 8px) centercalc(100% - 8px) center减号两侧的空白是语法要求,括号内的加减号一律不动
.card :focus-visible.card :focus-visible后代选择器,冒号前的空格有语义
opacity: 0.5 !importantopacity: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 automargin:0 auto只删冒号后那一个空白
content: " x "原样字符串内部整体保留
url( "a b.svg" )原样括号内整体保留
a{color:red;}a{color:red}右花括号前那条分号可省
--x: ;--x:;自定义属性空值,分号必须留
@charset "utf-8";原样后面不是右花括号,分号与引号都不动

另外三条与输入有关的具体限制:输入框里既没有 { 也没有 : 时,工具会判定「看起来不是 CSS」并报错,而不是返回一段被压得面目全非的文本;输入只有注释时压缩结果是空串,此时会提示你去补一条真实声明;输入超过 20 万字符会被拒绝,因为单次渲染这么大的结果会让输入框很难操作,拆成几段分别压缩更稳妥。

结果边界与字节口径

常见问题

为什么 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,这样改完还有测试兜底。

相关工具