CSS 压缩器,免费
通过移除注释和空白、优化值来压缩 CSS。
关于 CSS 压缩
人们很容易设想:既然 Brotli 对文本流的压缩如此激进,那么单独再做一遍压缩(minification)的边际价值就已经崩塌了。Sentry 工程团队博客在 2024 年 4 月正是论证了这个观点,得出的结论却恰好相反:压缩仍然值得做。全球移动互联网中位速度约为 53 Mbps,Brotli 已普遍部署在 CDN 边缘的情况下,「如果你能在每次页面加载时,从总共 100 个资源中只削掉 1 KB,对那些用户来说,你就可能在页面速度上换来 15 ms 的提升。」乘以任何非琐碎站点的访客基数,这就变成了一笔实实在在的带宽账单、移动设备上实实在在的电池开销,以及 Lighthouse 报告上一次实实在在的位置变化。
机制很重要。Gzip 和 Brotli 是通用的无损压缩器,它们寻找重复的字节序列,并把每一次重复编码为对一个滑动窗口字典的回引(back-reference)。两个产生相同浏览器行为但包含不同字节的 CSS 文件,会被压缩成两个不同的大小。margin: 10px 10px 10px 10px; 和 margin:10px 在语义上是相同的,但前者有更多需要编码的字符,尽管其中很多是重复的,Brotli 擅长寻找重复,但它并不理解两个语法上不同的 CSS 字符串表达的是同一条规则。只有具备 CSS 感知能力的minifier 才能做到这一点。Cloudflare 的学习中心把典型的原始文本压缩比放在 30-50 %;再叠加 Brotli 又会增加 5-15 % 的微小空间,但它并非零,而且在规模上,这会累积起来。
CSS 阻塞渲染,这就是字节为何重要的原因
Largest Contentful Paint 是三大 Core Web Vitals 之一;「Good」的阈值是页面加载第 75 百分位上的 2.5 秒。Lighthouse 的总体 Performance 评分把 LCP 的权重设为总分的 25 %。CSS 默认是渲染阻塞的,当浏览器看到一个 <link rel="stylesheet">,它就会暂停渲染,直到该文件被下载完毕、CSSOM 被构建出来,因为样式表中后面的规则可能覆盖前面的规则,浏览器不能冒险在未上样式或样式错误的状态下绘制。CSS Object Model 与 DOM 不同,不是增量构建的,解析器需要整张样式表才能知道哪些规则胜出。JavaScript 也会在此被阻塞:CSSOM 不完整时,脚本无法安全运行。实际意义在于,每一个渲染阻塞的 CSS 字节,都坐在每一项 Core Web Vital 的关键路径上。一个 100 KB 的 minified bundle,与一个 150 KB 未压缩的版本相比,在快速 4G 链路上能为你省下大约 30 ms,单页很小,但 Vodafone 的案例研究显示,LCP 改善 31 % 带来了销量 8 % 的提升。二阶效应更大:更快的 LCP 抬高了 Lighthouse 评分,进而对那些把 Web Vitals 作为已知排名信号的网站,提升了它们在 Google 搜索中的可见度。
CSS minifier 实际上在做什么
- 空白字符删除。仅为人类可读性而存在的空格、制表符和换行被移除。字符串(font-family 名称、content 值、URL)内部的空白以及用来分隔 token 的空白会被保留,
1px solid red不能变成1pxsolidred,因为解析器将不再识别这三个值。 - 注释删除。
/* ... */块会被删除。广为遵循的约定是:以/*!开头的注释被视为重要(通常是版权或许可声明)并被 cssnano、clean-css、lightningcss 和 esbuild 等生产级 minifier 所保留。 - 零单位折叠。
0px、0em、0%、0pt、0vw全都被简化为0,因为零长度无论单位为何都是同一个长度。需要小心的边界情况(Miriam Suzanne 的「Not All Zeros are Equal」,2022):calc()、min()、max()与clamp()内部的零,以及 CSS 自定义属性内部的零,都不能安全地剥掉单位,因为 CSS Values and Units Module Level 4 的类型化算术规则要求一个单位来区分长度与数值。transition中的0s不能变成0,因为该属性要求一个时间值。 - 十六进制颜色缩短。当一个六位十六进制颜色的每对数字相同时,它可以被写成三位:
#FFFFFF → #FFF、#336699 → #369。CSS Color 规范把三位形式定义为完全等价,自 CSS1 起便如此。 - 末尾分号删除。规则块中最后一条声明与右花括号之间的分号是可选的,所以
color:red;}变成color:red}。每条规则一个字节,累积起来不少。 - 关键字小写化。CSS 关键字大小写不敏感,所以
BLOCK和block含义相同,但小写更易压缩并能更统一地 tokenize。 - 简写折叠。
margin: 10px 10px 10px 10px变成margin: 10px;margin: 10px 20px 10px 20px变成margin: 10px 20px。生产级 minifier 知晓margin、padding、border、border-radius、background、font、transition、animation以及 grid 系列的简写规则。
Minifier 与 optimizer,「安全 vs 激进」的分界线
cssnano 的文档区分了它的「default」预设(只做安全变换)与「advanced」预设(做依赖于对你 CSS 的某些假设的激进变换)。默认预设是你不假思索就开启的;advanced 预设是你做完审计之后才开启的,因为有些优化会破坏依赖于细微级联行为、厂商前缀或浏览器怪癖的代码。Advanced 变换可以为提升可压缩性而在样式表内部对规则重排,如果你有意写下了像 display: flex; display: grid; 这样的重复属性回退,那就有风险。它们可以把相同的规则体合并到逗号分隔的选择器列表中。它们可以把被覆盖的声明当作死代码移除,同样有同样的回退保留。它们可以规范化 display 值、变换函数与自定义标识符,translate3d(0,0,0) 可以简化为 translate(0),如果你不依赖 GPU 提升的副作用。它们可以基于 Browserslist 目标移除未使用的厂商前缀。csso(Yandex,2011)把自己描述为「结构化优化器」而非纯 minifier,设有三类变换:清理、压缩、重构。Lightning CSS 描述了同样的划分:一个总是安全的「minify」步骤,加上一组 targets-aware 的变换,把现代 CSS 编译降级到旧浏览器能理解的内容。诚实的分界线是:minifier 是你可以不经思索地用在任何 CSS 文件上的东西;optimizer 是你结合对自己代码库的理解来使用的东西。
CSS minification 工具简史
YUI Compressor(2007)。Yahoo 的 Julien Lecomte 在 2007 年 8 月 11 日发布了 YUI Compressor。最初的版本只处理 JavaScript;三天后,2.0 版本通过整合一个最初由 Isaac Schlueter 为个人使用而写的、基于正则的 CSS minifier,增加了 CSS 支持。YUI Compressor 多年来一直是事实标准,尤其在 Java/Maven 和 Rails-asset-pipeline 的世界里;它至今仍在被使用,尽管 YUI 库本身在 2014 年被 Yahoo 退役。YUI Compressor 的 CSS 这一侧一直是基于正则的,而非基于 AST,这是一项设计抉择,其后果在之后出现的更小的浏览器端工具里回响。
clean-css(2011)。Jakub Pawlowicz 在 2011 年以 GoalSmashers 组织名义在 GitHub 上发布了 clean-css,把它定位为「适用于 Node.js 与 Web 的快速高效 CSS 优化器」。它是第一个专门为 Node.js 生态设计的主要 CSS minifier,并成为 Grunt 与 Gulp 流水线中的默认选择。目前的 5.x 版本线每周从 npm 拉取约 2100 万次下载,支持 @import 内联、URL 重新定位与 source map,并拥有分级的「优化级别」模型,使「安全 vs 激进」的划分变得明确。csso(2011)起源于 Yandex 的内部项目,以 2011 年版权由 Sergey Kryzhanovsky 基于 Vitaly Harisov 的想法发布。cssnano(2015 年 4 月)由 Ben Briggs 创建,采用了不同的思路:与其使用一个庞大的单体 minifier,cssnano 是一组精挑细选的小型 PostCSS 插件捆绑(默认预设中约 30 个)。它在 JavaScript 生态中成为主导的 CSS minifier,很大程度上得益于这种可组合性,以及 PostCSS 本身(Andrey Sitnik 的框架,首次发布于 2013 年 11 月)成为如此多其他事物(Autoprefixer、Stylelint、Tailwind 的处理流水线)的底层载体。cssnano 现在已发展到 7.x 版本,是webpack 的 css-minimizer-webpack-plugin、Next.js 以及许多其他框架默认设置中的默认 CSS minifier。
esbuild 的 CSS minifier(2020-2021)。Figma 联合创始人 Evan Wallace 把 esbuild 作为「我在 2019-2020 年寒假期间写的一个业余项目」发布。esbuild 用 Go 写成,比当时基于 JavaScript 的 bundler 快 10-100 倍。CSS 支持在 2021 年逐步成熟;它如今作为底层 minifier,在 Vite、tsup 以及无数框架模板中无处不在。Lightning CSS(2021/2022)。Parcel 的作者 Devon Govett 在 2021 年发布了 Parcel CSS,一个用 Rust 从零写就的 CSS 解析器、转换器、bundler 与 minifier。它的卖点是速度(比当时基于 JS 的既有工具快 10-100 倍)加上正确性(一个真正遵循 CSS 规范的解析器,而不是基于正则的模式匹配),再加上 targets-aware 的编译能力(将现代 CSS 降级到旧浏览器可理解的内容,就像 Babel 之于 JavaScript)。2022 年项目改名为 Lightning CSS,以便把它从 Parcel 中分离出来作为独立工具。它如今是 Vite 推荐的现代 CSS 流水线、Parcel 自身的默认选择,也是 css-minimizer-webpack-plugin 中的可选项。Tailwind 下一代 Oxide 引擎对其进行了集成。
现代 CSS 流水线
现代 Web 项目很少把原始 CSS 直接写进样式表。Sass(Hampton Catlin 与 Natalie Weizenbaum,首版 2006)、Less(Alexis Sellier,2009)以及 Stylus(TJ Holowaychuk,2010)位于流水线最前端,提供变量、嵌套、mixin、函数与 partial。编译器输出原生 CSS,接着流经 PostCSS,做诸如 Autoprefixer(基于 Browserslist 添加厂商前缀)、嵌套 CSS 展开、现代 CSS 降级等变换。只有在这一切之后,minifier 才会在最终扁平的 CSS 上运行。现代 bundler 默认就附带 CSS 压缩,webpack 5 包含 css-minimizer-webpack-plugin(默认底层使用 cssnano,可选 cssoMinify、cleanCssMinify、esbuildMinify 与 lightningCssMinify);Vite 默认对 CSS 使用 esbuild,并支持把 Lightning CSS 作为可选项;Parcel 使用 Lightning CSS;Next.js、Remix、Astro、SvelteKit 与 Nuxt 全部在生产构建中捆绑了压缩,无需开发者干预。结果是:对任何使用现代构建流水线的人来说,CSS 压缩自动发生。
但并不是每个人都使用构建流水线。许多 WordPress 站点、手写 HTML 页面、没有 JS 工具链的 Jekyll/Hugo/Eleventy 静态站,以及一次性的项目,所发布的 CSS 从未接触过 webpack。对这些情形,浏览器内的 minifier 是阻力最小的路径,把 CSS 粘贴进来,把结果复制出去,保存到部署里。
Critical CSS
一个值得了解的互补技术:critical CSS 指的是把渲染首屏视口所需的样式子集识别出来,直接内联到文档头部的 <style> 块中,然后把样式表的其余部分推迟为非阻塞加载的实践。做得好的话,页面会在第一次网络往返就把首屏视口绘制出来,完全不需要为外部样式表单独发请求。Filament Group 构建了规范化的工具,Scott Jehl 在 2014 年 7 月 14 日发布了 loadCSS,一个小巧的 JavaScript 实用工具,通过把样式表的 media 属性设为不匹配的值来异步加载它,等文件下载完成后再把它切回 all。这一模式最终标准到一定程度,以至于浏览器添加了 <link rel="preload" as="style" onload="this.rel='stylesheet'">,作为更直接的表达方式。Jason Miller(Preact 的作者)在 2018 年以 GoogleChromeLabs 名义发布了 Critters,作为更快的替代方案(它不去运行无头浏览器来检查渲染后的页面,而是做静态分析。Critters 仓库于 2024 年被归档,目前活跃维护的分支以 Beasties 之名存活于 Nuxt 团队下。所有这些工具都依赖于底层的 CSS 已经被良好压缩)源样式表中省下的每一个字节,都会传导到一个更小的关键内联块上。
遗留 hack 已经远去
现代浏览器都按同一份规范解析 CSS。那些曾让 CSS 压缩变得危险的历史性 hack,本质上都已消失:* html hack 通过利用一个解析器 bug 针对 IE6;下划线 hack _property: value 针对 IE6 及更早版本;*property: value(在属性名前加 *)针对 IE7 及更早版本;条件注释 <!--[if IE 6]> 让你能为特定 IE 版本提供不同的样式表,但 Microsoft 在 IE10 中移除了这个特性。IE11 在大多数消费版 Windows 10 上于 2022 年 6 月 15 日抵达生命周期终点。截至 2026 年,这些 hack 已经成了历史趣闻,CSS minifier 不再需要为它们操心。一个在各浏览器之间一致地剥离空白、并应用上述规范定义变换的现代 minifier 是安全的。
本工具做什么(以及不做什么)
本工具是一个单文件的浏览器内 minifier,大约 30 行 JavaScript。它把字符串字面量 token 化为占位符,以避免后续步骤破坏其内容,删除 /* ... */ 注释,折叠 {、}、:、;、,、>、~、+ 周围的空白,删除 } 之前的尾部分号,把连续空白折叠为单个空格,把每对数字相同的六位十六进制颜色缩短为三位形式,并对 px、em、rem、%、pt、ex、ch、vw、vh、vmin、vmax 的零值剥掉单位。它不把 CSS 解析成 AST(它是一次正则扫描。它不会把 longhand 折叠为 shorthand。它不会合并或去重选择器。它不会重排声明。它不会把 rgb() 转成 hex,也不会把命名颜色转成 hex。它不会把关键字小写化。它不会保留 /*! 的「重要」注释)所有注释一律删除;如果你有需要保留的许可头,请在压缩后再粘回去。它不会输出 source map。诚实的定位是:把从你编辑器或手写出的 CSS 粘进来,得到一个原始字节通常缩小 20-40 % 的精简版本,把它当作手工搭建站点的快速部署产物来用。对于带构建流水线的项目,请在那条流水线里使用 cssnano 或 Lightning CSS;对于没有的项目,本工具去除了这一摩擦。
为何「仅在浏览器」在这里很重要
每一个基于 Web 的 CSS minifier 都面临同样的架构选择:把工作放在服务器上做,还是放在用户的浏览器里做。服务器侧处理需要把样式表通过网络上传,这意味着该 CSS 的一份副本会留在服务器日志里,可能在 CDN 缓存里,可能在分析流水线里,可能在备份里。对大多数 CSS 来说这没什么影响。但对于内部工具、未发布的产品样式,或包含暴露内部类目分类的选择器的样式表(.admin-panel-internal-debug-info),情况就不一样了。即便是普通的营销站点 CSS,审计自己实际通过网络发送内容的开发者,也可以合理地希望自己的在制品留在本机上。一个纯粹基于浏览器的 minifier(在你标签页里运行的、初次页面加载之后再不发起网络请求的 JavaScript)回避了这个问题。你可以验证它:打开 DevTools 的 Network 面板,粘入 CSS,点击 Minify,然后留意有无任何外发请求。不会有。更进一步,在页面加载完成后断开互联网(或开启飞行模式),工具仍然可以工作,这是关于「没有任何东西被上传」最强的经验性证据。
常见问题
我的 CSS 会缩小多少?
带注释和缩进的手写 CSS,在原始字节上通常缩小 20-40 %。已经从 Sass/Less 编译并稍作格式化的 CSS 缩小得更少。已经经过 PostCSS 或 Autoprefixer 处理的 CSS,往往注释已经很少,缩小幅度最小。CDN 边缘的 Brotli 压缩之后,体积会进一步收窄,对压缩过的 CSS 用 Brotli,仍然能比对未压缩 CSS 用 Brotli 多省 5-15 %,具体取决于原始空白的重复程度。
压缩后的输出会让什么东西出问题吗?
对普通 CSS 来说,不会,所做的变换都是规范安全的。有两类情况值得你自己确认一下:(1)calc()、min()、max()、clamp() 与 CSS 自定义属性内部的零值不应被剥掉单位,因为类型化算术规则需要单位;本工具的正则扫描很保守,但如果你的 CSS 里有 calc(0px + 10%),请扫一眼输出。(2)如果你的样式表里有像 display: flex; display: grid; 这样的重复属性回退,本工具会把两者都保留下来,但带 advanced 预设的 cssnano 等更高级的 minifier 会把第一条移除掉。如果你依赖某种特定浏览器的级联行为,部署前请审计。
它会保留许可头吗?
不会。生产级 minifier 遵循一种约定:以 /*!(带感叹号)开头的注释会作为「重要」内容被保留下来,通常用于版权头与许可声明。本工具一视同仁地剥掉所有注释。如果你发布的 CSS 需要带许可头(MIT、GPL、BSD 源码归属),请把头手动粘回到输出里。如果你的流水线需要自动保留,请改用 cssnano 或 Lightning CSS。
如果我已经有构建流水线,是否还应该用这个?
大概率不需要,你的 bundler 已经在替你做这件事了。webpack 5 自带 css-minimizer-webpack-plugin,底层默认就是 cssnano;Vite 默认用 esbuild 来压缩 CSS;Parcel 用 Lightning CSS。这个工具是为你的构建流水线没有覆盖的场景准备的:手写的 HTML、没有 Node 工具链的 WordPress 主题、不打包压缩的静态站点生成器、为某个邮件模板或快速 demo 写的一次性 CSS。对这些情形来说,浏览器里粘贴即可的工具是阻力最小的路径。
我的文件会被上传吗?
不会。这个 minifier 是运行在你浏览器里的 JavaScript。你粘进来的 CSS 永远不会跨过网络,你可以在 DevTools 的 Network 面板里一边点 Minify 一边自己确认,或者在页面加载后让它离线,确认工具仍能工作。内部样式表、未发布的产品样式,以及暴露内部类目分类的 CSS,都会留在你的设备上。
我以后能把输出反压缩回去吗?
并不能完全做到,注释和原始空白已经永久消失,这本就是目的所在。但你可以把压缩后的 CSS 格式化,让它再次可读。Prettier(配合 CSS 插件)、VS Code 内置的 Format Document 命令,以及任何在线的「CSS beautifier」类工具,都会重新插入缩进与换行,但它们无法恢复已经被删除的注释。请始终把未压缩的源文件保存在版本控制中,作为正式形态。