JS 中的 Minification 和 Minifier

英文里有很多单词目前目前还没有翻译恰当的中文词汇与它们对应,导致平时学习和沟通时产生混淆。对于前端开发者来说,日常使用第三方库时一定注意过同时存在 *.js*.min.js*.css*.min.css 这样成对命名的文件,将前者处理成后者的过程,即“Minification”,中文语境一般称为“压缩”。与此同时,使用压缩软件压缩文件体积的过程,即“Compression”,也被称之为“压缩”。所以“把这些 JS 文件压缩一下”就成了一个考验你语言理解能力的玄学问题……

1 Minification

Minification 是动词 minify 的名词形式(minify 的意思是“to minimize or lessen the size of something”,也就是“最小化或者减小某物的尺寸大小”),这里把它翻译成“最小化”,而不是常规的“压缩”。将代码最小化的工具被称为“minifier”,既然与它对应的“magnifier”被译作“放大器”,那么这里也不妨把“minifier”翻译成“缩小器”。

Wikipedia 上这么解释编程中的最小化概念:最小化是指从解释型编程语言、标记语言源码中移除所有不必要字符(且不改变原有的功能)的过程。这些不必要字符通常包括空白符、换行符、注释、块分隔符(比如大括号),它们只是用于增加代码的可读性,执行的时候是非必须的。最小化可以减小源码的大小,使得网络传输时更加高效。

不要将最小化和代码混淆(Code Obfuscation)、数据压缩(Data Compression)混为一谈。最小化的代码很容易通过代码格式化、美化工具逆向处理。最小化的代码也不需要经过解压就能和源码一样被解释器解释执行。

2 Minification in JS

先从一个例子直接感受下什么是最小化,以下示例代码摘自 2001 年 Douglas Crockford 对 JSMin 项目的介绍。

// is.js

// (c) 2001 Douglas Crockford
// 2001 June 3


// is

// The -is- object is used to identify the browser.  Every browser edition
// identifies itself, but there is no standard way of doing it, and some of
// the identification is deceptive. This is because the authors of web
// browsers are liars. For example, Microsoft's IE browsers claim to be
// Mozilla 4. Netscape 6 claims to be version 5.

// Warning: Do not use this awful, awful code or any other thing like it.
// Seriously.

var is = {
    ie:      navigator.appName == 'Microsoft Internet Explorer',
    java:    navigator.javaEnabled(),
    ns:      navigator.appName == 'Netscape',
    ua:      navigator.userAgent.toLowerCase(),
    version: parseFloat(navigator.appVersion.substr(21)) ||
             parseFloat(navigator.appVersion),
    win:     navigator.platform == 'Win32'
}

is.mac = is.ua.indexOf('mac') >= 0;

if (is.ua.indexOf('opera') >= 0) {
    is.ie = is.ns = false;
    is.opera = true;
}

if (is.ua.indexOf('gecko') >= 0) {
    is.ie = is.ns = false;
    is.gecko = true;
}

最小化后:

var is={ie:navigator.appName=='Microsoft Internet Explorer',java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',ua:navigator.userAgent.toLowerCase(),version:parseFloat(navigator.appVersion.substr(21))||parseFloat(navigator.appVersion),win:navigator.platform=='Win32'}
is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true;}
if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;}

目前以 UglifyJS 为代表的 minifier,对代码的处理已经远远不止移除空白符、换行符、注释、块分隔符这么简单,UglifyJS 会解析 JavaScript 代码,构建抽象语法树(AST),优化 AST,还可以将局部变量重命名为字母、移除没有引用到的变量,最终生成文件体积更小的 JavaScript 代码。

UglifyJS 包含以下部分:

  • A parser which produces an abstract syntax tree (AST) from JavaScript code.
  • A code generator which outputs JavaScript code from an AST, also providing the option to get a source map.
  • A compressor (optimizer) — it uses the transformer API to optimize an AST into a smaller one.
  • A mangler — reduce names of local variables to (usually) single-letters.
  • A scope analyzer, which is a tool that augments the AST with information about where variables are defined/referenced etc.
  • A tree walker — a simple API allowing you to do something on every node in the AST.
  • A tree transformer — another API intended to transform the tree.

摘录自 http://lisperator.net/uglifyjs/

3 History

自从 2001 年 JSMin 发布以来,涌现了不少新的 minifier。

  • 2006 年,Dojo Toolkit 发布基于 Java 和 Rhino 的 ShrinkSafe。
  • 2007 年,Yahoo! 发布基于 Java 和 Rhino 的 YUI Compressor。
  • 2009 年,Google 发布基于 Java 和 Rhino 的 Closure Compiler,实现了 Source Map 特性。
  • 2009 年,Microsoft 的 ASP.NET 团队发布 Microsoft Ajax Minifier。
  • 2010 年,Mihai Bazon 发布基于 Node.js 的 UglifyJS1。
  • 2012 年,Mihai Bazon 发布重写版的 UglifyJS2,支持 Source Map。
  • 2015 年,Babel 团队 发布 Babili,后更名为 babel-minify,支持 ES2015 语法。
  • 2016 年,Google 发布 JavaScript 版的 Closure Compiler,支持 ES2015 语法(注:非重写版,而是将 Java 源码转换成了 JavaScript)。
  • 2017 年,Rich Harris 发布 Butternut,支持 ES2015 语法。

注:

  1. Rhino 是 Mozilla 开源的使用 Java 实现的 JavaScript 引擎。
  2. 2008年,Google Chromium 团队开源使用 C++ 实现的 JavaScript 引擎 V8
  3. 2009 年,Ryan Dahl 发布基于 V8 引擎的 Node.js
  4. Source Map 目前还没有独立的官网和 GitHub 主页,提案都放在 Google Docs 上。

4 JS Minifiers 及其生态

下面列出了一些常见的 minifier,可以搭配 webpack、Rollup、Gulp 等构建工具使用。

  • JSMin: The JavaScript Minification Filter.
  • ShrinkSafe: ShrinkSafe is a standalone Java-based JavaScript compressor which utilizes Rhino to parse code and safely shorten the results.
  • YUI Compressor: The Yahoo! JavaScript and CSS Compressor.
  • Google Closure Compiler: Check, compile, optimize and compress Javascript with Closure-Compiler using Java.
    • closure-compiler-js repo: Check, compile, transpile, optimize and compress JavaScript with Closure Compiler in JS.
    • closure-compiler-npm repo: Package for managing and documenting closure-compiler for use via npm.
      • google-closure-compiler
      • google-closure-compiler-java (compiler.jar)
      • google-closure-compiler-js (jscomp.js)
      • google-closure-compiler-linux (compiler)
      • google-closure-compiler-osx (compiler)
      • google-closure-compiler-windows (compiler.exe)
  • Microsoft Ajax Minifier (AjaxMin)
  • UglifyJS (uglify-js): JavaScript parser, minifier, compressor and beautifier toolkit.
    • uglify-es: A JavaScript parser, mangler, compressor and beautifier toolkit for ES6+. (deprecated)
    • Terser: JavaScript parser, mangler, optimizer and beautifier toolkit for ES6+
  • Esmangle: mangler / minifier for Mozilla Parser API AST
  • Babili (babel-minify): An ES6+ aware minifier based on the Babel toolchain.
  • Butternut: The fast, future-friendly minifier.

5 JS Minifiers Benchmarks

可以参考 Benchmarks - babel-minify

6 常用 JS 库、框架使用哪些 minifier?

7 JS Minifiers 使用中遇到的问题

7.1 服务器使用了 gzip 还需要再用 minifier 处理吗?

建议将 minification 和 gzip 搭配使用,可以参考 The Difference Between Minification and Gzipping - CSS-Tricks

7.2 webpack/Rollup 支持 Tree Shaking,还需要再用 minifier 处理吗?

webpack/Rollup 支持作用域提升(Scope Hoisting)和 Tree Shaking,但还是需要 minifier 才能发挥效果。

7.3 在打包工具中使用 UglifyJS/Terser 报错或者卡住

ERROR in vendor.js from UglifyJs Unexpected token: name (fn) [vendor.js:37118,6]

这种错误一般是因为 UglifyJS 目前只支持 ES5,可以切换到 Terser,或者使用 Babel 预先转译代码(包括 node_modules 中相关的模块)。

92% chunk asset optimization TerserPlugin

这种错误一般是因为引入了 *.min.js 文件,UglifyJS/Terser 处理 minified 代码会导致性能下降,建议在打包工具的配置中对相关 minified 文件进行排除。可以参考 uglify 压缩报错问题及 es5-imcompatible-versions - 知乎专栏

7.4 UglifyJS/Terser 没有 changelog ?!

注意:Terser 从 v3.15.0 开始提供 changelog

UglifyJS 目前并没有 changelog 或者 release notes,一方面维护者没有精力撰写,另一方面一直有用户请求提供,同时维护者指责没有人愿意贡献。除了 changelog,UglifyJS 还存在不遵循语义化版本号规则的问题,例如 3.2.2 升级到 3.3.2 会出现不兼容。

相关 Issues:

8 相关链接