JavaScript 压缩器,免费

通过移除注释、空白和不必要字符来压缩 JavaScript 代码。

关于 JavaScript 压缩

JavaScript 是大多数网页中最重的资源类型,这不仅仅因为它通常是最大的文件。成本分为五个阶段:下载(通过网络)、解析(引擎读取字节并构建 AST)、编译(V8 或 JavaScriptCore 将 AST 编译为字节码)、执行(字节码运行),以及后续访问时,如果引擎保留了它,则从缓存的字节码暖启动。CDN 边缘的 Brotli 处理下载成本。压缩对下载也有帮助,但它也对每个下载你脚本的设备上的解析和编译有帮助,在低端 Android 手机上,解析和编译可能比网络传输花费更长时间。Addy Osmani 的 Cost of JavaScript 文章反复表明,看起来像网络问题的往往是 CPU 问题,而压缩 20-40% 的字节减少相当直接地转化为在慢速设备长尾中节省的解析时间毫秒数。

V8 的惰性解析优化将任何函数的完整解析推迟到它实际被调用时,这意味着具有许多未使用函数的大型脚本的成本低于其字节数所暗示的。Chrome 的 HTTP 缓存也存储 V8 字节码("Code Cache"),因此重复访问完全跳过解析和编译阶段。这些都不改变基本等式:更小的脚本更快命中暖缓存,在冷路径上解析更快,编译更快。压缩是整个栈中夺回毫秒最便宜的地方。

JavaScript 压缩的四个(或五个)级别

并非所有压缩器都相同,你需要的级别取决于你的代码库。级别 1:空白和注释删除。最简单的过程,剥离空格、制表符、换行符和 // ... / /* ... */ 注释。这是正则表达式过程可以安全执行的工作(在字符串、模板字面量和正则字面量周围要小心)。典型减少:原始字节的 20-40%。级别 2:符号重命名(mangling)。局部变量名、函数参数和(谨慎地)函数名被重写为单字母:function calculateTotal(itemList) 变成 function a(b)。这需要抽象语法树和完整的作用域分析,以了解哪些名称可以安全重命名,全局名称、导出、对 this 属性的引用以及通过字符串键访问(obj['name'])到达的任何内容都必须保持完整。典型额外减少:10-25%。级别 3:死代码消除(tree shaking)。模块级静态分析识别从未执行的导入和代码路径并将其删除。需要模块级类型信息和对副作用的清晰理解。典型额外减少:可变,但在交付许多功能的库上可能巨大。级别 4:内联和常量折叠。2 * 60 * 60 这样的简单表达式在构建时被求值为 7200;被调用一次的小函数可能被内联到其调用者中。级别 5:属性 mangling。最激进的优化,对象属性名也被重写。破坏任何使用字符串键(obj['name'] vs obj.name)或将属性名暴露为其公共契约一部分的代码。几乎总是选择性启用,几乎总是通过 --mangle-props 正则限制为特定标识符。

JavaScript 压缩工具的简史

JSMin (2003)。Douglas Crockford,是的,就是那位推广 JSON 的 Crockford,在 2003 年用 C 编写了 JSMin。它是一个微小的单文件程序,执行基本的空白和注释删除,没有 AST、没有作用域分析,并对 ASI(自动分号插入)边缘情况采取刻意保守的方法。它树立了"最简单可行之事"的标杆,是此后每个基于正则的 JS 压缩器的精神祖先。YUI Compressor (2007)。Yahoo 的 Julien Lecomte 于 2007 年 8 月 11 日宣布了 YUI Compressor。它使用 Rhino 分词器进行安全的符号重命名,是第一个广泛使用的为 JavaScript 进行真正基于 AST 的 mangling 的 Java 工具。Closure Compiler (2009)。Google 自 2005 年起在内部使用;他们于 2009 年 11 月 5 日在 Apache 2.0 下开源。Closure 是那个时代最激进的优化器,通过 JSDoc 注释了解类型,具有可以基于类型推断重写属性名的"advanced"模式。代价是你必须以 Closure 友好的方式编写代码;高级模式失败是出了名的。

UglifyJS (~2010-2012)。Mihai Bazon 的 UglifyJS 是第一个 JavaScript 原生压缩器,用 JS 编写,在 Node.js 上运行,它成为了十年来的 npm 默认。UglifyJS 2 添加了 source map 支持和 ES5 功能;UglifyJS 3 继续 ES5 打磨,但从未完全获得 ES6+ 支持。Terser (2018 年 8 月)。Fabricio Matté 将 UglifyJS 分叉为 Terser,专门用于添加 ES6+ 支持,而不破坏长期稳定的 UglifyJS API。Terser 现在是 webpack 5、Rollup、Parcel 1 和较旧 Next.js 版本中的默认 JS 压缩器。swc (2017/2019)。Donny "kdy1" Choi 的 swc("Speedy Web Compiler")是一个基于 Rust 的 JavaScript/TypeScript 编译器,内置压缩器比 Terser 快 20-70 倍。Next.js 从 2021 年 10 月的版本 12 开始将其默认压缩器从 Terser 切换到 swc。esbuild (2019-2020 年冬)。Figma 联合创始人 Evan Wallace 在 2019-2020 年冬假期间发布了 esbuild。用 Go 编写,它比当时基于 JavaScript 的打包器快 10-100 倍,并附带自己的压缩器。esbuild 现在是 Vite、tsup 和许多框架模板中的底层压缩器。过去五年的总体方向是:用快速系统语言(Rust 或 Go)编写的解析器、基于 AST 的优化、智能的 ES 模块感知 tree shaking。浏览器粘贴的正则压缩器,如这个工具,位于该阶梯的底部,做着仍然有用的最简单工作。

压缩 JavaScript 的 Source Map

Source map 是一个 JSON 旁路文件,将压缩输出中的位置映射回原始源中的位置。Source Map V3 规范由 John Lenz(Google)和 Nick Fitzgerald(Mozilla)于 2011 年起草,并于 2024 年 6 月被 TC39 采用为 ECMA-426,同样的 source-map 格式适用于 JavaScript 和 CSS。浏览器通过压缩文件中的尾随注释消费该地图://# sourceMappingURL=app.js.map。当 DevTools 打开且获取到地图时,Sources 面板显示原始源,断点、控制台错误和堆栈跟踪都引用回它。生产压缩器(Terser、swc、esbuild、Closure)都按需发出 source maps。这个工具不,对于一个返回文本而不是可下载文件对的浏览器内一次性工具,source maps 增加了显著的复杂性,而收益甚微。诚实的披露是,这个工具是单向通过;构建管道压缩器对 source maps 有更强的论据,因为原始源在磁盘上,而开发者需要在运行时调试。

现代打包器默认值,大多数人已经有了压缩器

如果你使用现代构建管道,你的压缩器已经在运行。webpack 5 默认使用带 Terser 的 terser-webpack-pluginVite 默认使用 esbuild 进行压缩;CSS 用 Lightning CSS。Parcel 使用 swc。Next.js 在 v12(2021 年 10 月)中从 Terser 切换到 swc,并在同一版本中将整个构建管道从 Babel 切换到 swc。Remix、Astro、SvelteKit、Nuxt、Rollup、独立的 esbuild,都将压缩捆绑到生产构建中,无需开发者干预。结果是,对于任何使用现代构建管道的人,JS 压缩会自动发生,优化远超单文件正则工具能做到的。这个浏览器内压缩器赚取其位置的情况:带有内联 <script> 块的手工 HTML 页面;不带 Node 工具链交付的 WordPress 主题;不捆绑压缩的静态站点生成器;粘贴到 CMS 或邮件模板中的一次性片段;设置构建管道比脚本本身花费时间更长的快速实验。

诚实的范围:此工具做什么和不做什么

这个工具是一个基于正则的压缩器,大约 30 行 JavaScript。它将字符串字面量、模板字面量和正则字面量分词为占位符,以便后续转换不会损坏其内容;剥离 // .../* ... */ 注释;折叠空白序列;删除不需要它们的标点周围的空白;并恢复分词后的字面量。典型输出在原始字节中比输入小 20-40%。这个工具做、而生产压缩器(Terser、swc、esbuild、Closure)处理的:它将局部变量重命名为单字母(无作用域感知 mangling);它执行死代码消除或 tree-shaking;它执行常量折叠或表达式简化;它发出 source maps;它理解 TypeScript 语法(只粘贴纯 JavaScript);它 tree-shake ES 模块导入;它重写属性名。诚实的框架:粘贴从你的编辑器或你的手出来的 JavaScript,获得一个通常在原始字节中小 20-40% 的剥离版本,并将其用作快速部署工件。对于有构建管道的项目,在该管道中使用 Terser、swc 或 esbuild;AST 感知优化是这个工具 20-40% 减少与生产压缩器 60-80% 之间的差异。

正则压缩器的陷阱

不理解 JavaScript 语法的基于正则的过程可能以微妙的方式损坏代码。经典陷阱:模板字面量使用反引号,可以包含插值表达式(`Hello ${name}!`),如果正则不小心,看起来像注释剥离候选。正则字面量/^\/\*/g 包含正斜杠、类注释序列和类字符串内容;处理不当会将正则变成语法上损坏的代码。包含类注释文本的字符串(const url = "// example.com"),朴素的注释删除会剥离 // 之后的所有内容。ASI 陷阱,自动分号插入是允许你大部分时间省略分号的 JS 功能,但当下一个标记以括号、方括号或运算符开头((/regex/)[arr]+1)时,它与空白剥离交互不良,不谨慎的空白折叠可以将"两条语句"变成"一条带解析错误的语句"。此工具中的缓解措施是首先运行的字面量分词过程,用唯一占位符替换每个字符串和正则,在清理后的代码上进行注释+空白工作,然后恢复占位符。它并不完美,但涵盖了常见情况。如果你怀疑压缩器破坏了你的代码,首先要检查的是包含分词器遗漏的斜杠序列的正则字面量。

隐私:为什么仅浏览器在此处重要

服务器端 JS 压缩器(将你的代码 POST 到服务器、在那里通过 Terser 运行并返回结果的在线工具)需要上传你的源代码。对于普通的库代码,这是无害的。对于内部工具、未发布的产品代码、包含内联 API 密钥或第三方服务凭据的 JavaScript,或任何揭示专有算法或业务逻辑的代码,则不是。一个纯浏览器的压缩器,在你的标签页中运行的 JavaScript,在初始页面加载后从不发出网络请求,绕开了这个问题。你可以通过打开 DevTools 的 Network 标签、粘贴代码、单击 Minify 并观察任何传出请求来验证。更好的是,在页面加载后断开互联网(或启用飞行模式),工具仍将工作,这是没有任何东西被上传的最有力的经验证明。

常见问题

我的代码会小多少?

对于带有注释和缩进的手工格式化代码,期望原始字节大小减少 20-40%。具有完整 AST 优化的生产压缩器(Terser、swc、esbuild)达到 60-80%,差距是符号重命名、死代码消除和常量折叠,纯正则工具都不能安全地做到。在 CDN 边缘进行 Brotli 压缩后,压缩的额外节省更适度(在未压缩原始的 Brotli 基础上 5-15%),但它不是零,在规模上会累加。

压缩输出会破坏我的代码吗?

对于绝大多数代码,不会。已知的边缘情况围绕带有斜杠序列的正则字面量(/foo\/bar/)、跨行嵌入插值的模板字面量,以及剥离换行会改变解析的自动分号插入上下文。该工具的字面量分词过程处理常见情况,但如果你的代码在任何这些模式中异常密集,请在部署之前在浏览器中测试压缩输出。对于生产构建管道,使用 Terser 或 swc,它们具有完整的 AST 感知,可以正确处理这些情况。

此工具会重命名变量吗?

不会。变量 mangling,将 function calculateTotal(itemList) 转换为 function a(b),需要在 AST 上进行完整的作用域分析以了解哪些名称可以安全重命名。正则过程不能安全地做到这一点。对于符号重命名,在构建管道中使用 Terser、UglifyJS 或 swc;它们正确实现了作用域感知 mangling。它产生的 10-25% 额外大小减少是真实的,值得为生产代码做。

我可以粘贴 TypeScript 吗?

不,这个工具是仅 JavaScript 的压缩器。TypeScript 添加了类型注释(function add(a: number, b: number): number)、接口、枚举、装饰器和其他不是有效 JavaScript 的语法。首先使用 tsc、swc、esbuild 或 Babel 将你的 TypeScript 编译为 JavaScript,然后将 JavaScript 输出粘贴到这里。大多数 TypeScript 项目已经将其中一个编译器作为构建的一部分通过,所以 JavaScript 存在于某处。

如果我已经有构建管道,我应该使用这个吗?

可能不,你的打包器正在为你做这件事,具有远超正则工具能提供的基于 AST 的优化。webpack 5 附带带 Terser 的 terser-webpack-plugin;Vite 默认使用 esbuild for JS;Parcel 使用 swc;Next.js 自 v12(2021 年 10 月)起使用 swc。这个工具适用于你的构建管道未覆盖的情况:手工 HTML 页面、不带 Node 工具链交付的 WordPress 主题、不捆绑压缩的静态站点生成器、一次性片段,或设置构建比脚本本身花费时间更长的快速实验。

我的文件会被上传吗?

不。压缩器是在你的浏览器中运行的 JavaScript。你粘贴的代码从不跨越网络,在单击 Minify 时在 DevTools 的 Network 标签中验证,或在页面加载后将其下线并确认工具仍然工作。内部工具、未发布的产品代码、包含内联 API 密钥或专有业务逻辑的脚本都留在你的设备上。

相关工具