CSS & JavaScript 压缩器

压缩 CSS 和 JavaScript 代码

压缩器到底做了什么——经过 gzip 之后还剩多少收益

真正的 JavaScript 压缩器(terser、esbuild、swc)做的远不止删空白。它们先把源码解析成 AST,再施加保持语义的变换:标识符改名(把局部变量改成单字母——因为作用域已知,所以安全)、常量折叠(1000 * 60 * 60 变成 3600000)、死代码消除(if (false) 分支直接消失)、布尔值与属性简写重写(true 变 !0,obj['key'] 变 obj.key)、函数内联。带 pure 注释标记的调用如果结果未被使用可以整体删除——tree-shaking 后的库能达到宣传的体积,靠的正是这个。 数字之所以重要,是因为 gzip 会改变结论。仅压缩本身通常能去掉应用代码 30–60% 的原始字节。但 gzip 极擅长压缩空白和重复的长标识符,所以压缩后差距会缩小:一个从 400 KB 压到 160 KB 的文件,gzip 后可能是压缩版 52 KB 对未压缩版 61 KB。剩下的 15–20% 依然值得要——而且改名对压缩率的帮助比想象的小,因为短名字削减的冗余本来 gzip 也能利用。 实用规则:测量压缩后的体积,而不是原始体积。一个能省 40% 原始字节但 gzip 后只省 3% 的压缩步骤,可能不值得为它引入 source map 的复杂性。反过来,死代码消除和 tree shaking 往往比所有空白删除加起来省的压缩字节还多——因为被删掉的代码压缩后就是零。
# same bundle, four ways (typical mid-size app)
source                412,000 bytes
minified              168,000 bytes   (-59% raw)
minified + gzip        52,000 bytes
minified + brotli-11   46,000 bytes

gzip WITHOUT minify  ≈ 61,000 bytes
# → minification still buys ~15% after compression,
#   mostly from dead-code elimination, not whitespace

正则 vs AST:为什么字节级压缩 JavaScript 不安全

JavaScript 无法用字符串替换安全压缩,原因是结构性的。第一,斜杠字符有歧义:a / b 是除法,而 /b/.test(a) 是正则字面量。某个斜杠到底是哪种,取决于解析状态,而基于正则的替换器没有这个状态。把 /https?:\/\// 这类正则字面量里的"注释"删掉,代码语义就悄悄变了。 第二,自动分号插入(ASI)让换行具有语义。return 后跟换行再跟对象字面量会返回 undefined——ASI 在 return 后插入了分号。粗暴地合并行要么掩盖这个 bug,更糟的是制造新 bug:以变量结尾的一行和以左括号或左方括号开头的下一行拼在一起,会融合成函数调用或属性访问。真正的压缩器先解析,因此知道哪里必须有分号,甚至能反过来利用 ASI 安全地省掉分号。 第三,字符串和模板字面量是不透明容器。含两个斜杠的字符串不是注释;模板字面量里的空白是数据,必须逐字节保留。 对这类快捷在线压缩器的结论:用于事后能人工过目的小片段——书签脚本、内联事件处理器、短样式块——没问题。凡是要发布给用户的代码,请在构建中使用 terser、esbuild 或 swc。esbuild 比 terser 快大约 10–100 倍,输出只大约 1–2%;terser 压得最狠;swc 居中,且对 TypeScript 支持出色。
// 1. ASI: this returns undefined, not the object
function pick() {
  return          // <- semicolon auto-inserted here
    { ok: true };
}

// 2. line joining creates a call that never existed
const total = fn
(a || b).forEach(log)   // joined: fn(a || b).forEach(log)

// 3. slash ambiguity — is it division or a regex?
const x = a / b / c;        // two divisions
const ok = /a\/b/.test(s);  // one regex containing a slash
// a byte-level replacer cannot tell these apart

CSS 压缩:安全的变换与 calc() 陷阱

CSS 比 JavaScript 简单,所以更多部分可以机械地压缩——但不是全部。可靠安全的变换有:删除注释(以 /*! 开头的许可证注释除外,构建工具按惯例保留)、折叠字符串之外的空白、去掉每个声明块最后一个分号、缩短成对数字的六位十六进制颜色(#ffffff 变 #fff)、去掉零长度的单位(0px 变 0——因为零在任何长度单位下都是零)、合并四个相同的盒模型值(margin: 4px 4px 4px 4px 变 margin: 4px)。 经典陷阱是 calc():CSS 语法要求 calc 内的 + 和 - 两侧必须有空白,因为紧贴数字的减号会被解析成负值而不是减法。calc(100% - 20px) 合法;calc(100%-20px) 不合法,浏览器会悄悄丢弃整条声明。类似的空白敏感性也存在于媒体查询(screen and (min-width: ...))和 CSS 自定义属性中——自定义属性的值按 token 原样存储,压缩 var() 回退值或自定义属性值的内部,可能改变使用方读到的内容。 另一个不直观的点:0% 并不总能替换成 0(在 flex-basis 和某些动画上下文中百分号有意义);去掉字体名或 url() 值的引号,只有在内容不含保留字符时才安全。 实际管线中由 cssnano(基于 PostCSS,配置极细)或 Lightning CSS(Rust 编写,极快,还能把现代语法转译给旧浏览器)处理。两者都解析成 AST,所以既能安全执行上述变换,也能在上下文不允许时跳过。
/* before */
.card { margin: 0px 0px 0px 0px; background: #ffffff; }
.col  { width: calc(100% - 20px); }

/* correct minify */
.card{margin:0;background:#fff}.col{width:calc(100% - 20px)}

/* BROKEN: whitespace inside calc() is syntax, not formatting */
.col{width:calc(100%-20px)}   /* declaration silently dropped */

/*! preserved license header — the ! is the convention */

Source Map:压缩的同时不丧失调试能力

指向 bundle.min.js 第 1 行第 48213 列的生产环境堆栈毫无用处。Source map 解决了这个问题:.map 文件(内含 VLQ 编码 mappings 字符串的 JSON)为每个生成后的位置记录原始文件、行、列,通常还有原始标识符名。压缩文件通过最后一行的 sourceMappingURL 注释引用它,浏览器开发者工具按需加载——普通访客永远不会下载。 部署时的问题是谁能看到 map。三种常见方案:(1)公开 map,最简单,但任何好奇的人都能看到你的原始源码;(2)隐藏 map——照常生成,但不在 bundle 中引用,而是上传到错误追踪平台(Sentry、Datadog、Rollbar),由服务端符号化堆栈;(3)仅向通过认证的内网 IP 提供的受限 map。闭源产品的标准答案是隐藏 map;注意嵌入 sourcesContent 的 map 文件本身就包含完整的原始源码文本。 两个实战坑。链路上的每个变换步骤都必须生成并消费 map(TypeScript 到 esbuild 到 terser),否则最终 map 指向中间产物,所有栈帧都解析到错误位置。其次,重新发布时 map 必须与生成它的 bundle 逐字节对应——错误追踪平台用发布标识或内容哈希来关联 map 正是为此。如果符号化后的堆栈看着合理却微妙地不对,十有八九是发布版本错配的过期 map 在作怪。
// last line of bundle.min.js
//# sourceMappingURL=bundle.min.js.map

// hidden-maps workflow (Sentry example)
esbuild app.ts --minify --sourcemap=external --outfile=dist/app.js
sentry-cli sourcemaps upload --release=v1.4.2 ./dist
rm dist/*.map        # maps never reach the CDN

// stack trace before / after symbolication
at t (bundle.min.js:1:48213)
at checkout (src/cart/checkout.ts:87:11)   // with map + matching release

压缩、传输压缩、打包:三件不同的事,三件都要做

团队经常把三种作用于不同层面的优化混为一谈。压缩(minify)在构建时一次性重写源码文本;传输压缩(gzip 或 Brotli)在响应时对字节编码,浏览器透明解码;打包(bundle)合并模块以减少请求开销并支持跨模块 tree shaking。三者效果相乘——谁也替代不了谁。 传输压缩的配置比多数人想的更重要。静态资源应在构建时以最高级别预压缩(Brotli 质量 11、gzip 9),让服务器通过内容协商选择编码;Brotli-11 按请求实时跑太慢,但部署时跑一次几乎零成本。动态响应用 Brotli 4–5 或 gzip 6——更高级别烧掉大量 CPU 只换来个位数百分比的收益。典型文本压缩率:gzip 约 3–4 倍,Brotli 在 JS/CSS 上比 gzip 再小约 10–20%。 针对本工具服务的内联片段工作流,操作顺序是:可读源码留在仓库里,只对要粘贴进 HTML 属性、GTM 容器或邮件模板的副本做压缩,并验证结果仍能解析——JS 贴进浏览器控制台,CSS 用校验器。永远不要把压缩后的文本当作可编辑的原本:随时可以重新生成的单向产物无害,而手工修改过的压缩代码块是无法撤销的技术债。
最后更新:

关于此工具

CSS / JavaScript 压缩器去除注释、空白和冗余标记,缩减生产文件大小,提升加载和解析速度,降低 CDN 成本。构建管线通常会自动处理,但内联片段、邮件样式、不经过打包器的小脚本仍然需要快速在线压缩。

使用方法

  1. Choose CSS or JavaScript mode at the top.
  2. Paste your source into the input panel.
  3. Click Minify — the output appears on the right with the size saving in bytes and percent.
  4. Copy the minified result to embed in a <style> or <script> tag.
  5. Use the original source as the long-term editable copy and only ship the minified version.

常见用例

  • Inlining critical CSS in the <head> for faster First Contentful Paint.
  • Shrinking a Google Tag Manager custom HTML snippet to the size limit.
  • Compacting a JS one-liner you want to embed in a bookmarklet.
  • Reducing the size of email-safe CSS where every byte matters.
  • Trimming a no-bundler vanilla JS file before deploying to a static host.
  • Estimating the bundle-size impact of a snippet before adding it to a build.

常见问题

Q. Is this safe for production JavaScript?

A. For trivial scripts yes. For real applications use a battle-tested minifier like esbuild, terser, or swc inside your build pipeline so source maps and ECMAScript edge cases are handled.

Q. Why does the output break my CSS?

A. Most often a missing semicolon in the source. CSS minification assumes valid input — fix the source, then minify.

Q. Does minifying obfuscate my code?

A. Slightly — names are not changed, only whitespace is stripped. For real obfuscation use a dedicated tool, but understand it does not provide real security.

Q. Can I minify HTML the same way?

A. HTML minification needs different rules (preserved whitespace in <pre>, attribute quoting, etc.). Use a dedicated HTML minifier for that.