[译]Uglify vs Babel-minify vs Terser 一场代码压缩的pk

12,785 阅读12分钟

[译]Uglify vs. Babel-minify vs. Terser: 一场代码压缩的战役

原文链接-uglify-vs-babel-minify-vs-terser-a-mini-battle-royale

什么是缩小?

缩小(也是最小化)是从 解释的编程语言标记语言 的源代码中删除所有不必要的字符而不改变其功能的过程。这些不必要的字符通常包括

  • 空白字符
  • 换行符
  • 评论
  • 阻止分隔符

让我们试着通过一个例子来理解这一点。下面的代码显示了一个示例 JavaScript 代码,用于创建数组并使用前20个整数值对其进行初始化:

var array = []; for(var i = 0 ; i < 20 ; i ++){ array [ i ] = i ; }   

现在,让我们尝试手动缩小这些代码。下面的示例显示了我们如何只使用一行代码来实现相同的功能:

for(var a = [ i = 0 ]; ++ i < 20 ; a [ i ] = i );

首先,我们减少了 array 变量的名称(array to a),然后我们将它移动到循环初始化构造中。我们还将第 3 行的数组初始化移动到循环中。结果,字符数和文件大小显着减少。

为何要缩小?

现在我们了解缩小是什么,很容易猜到我们为什么这么做。由于缩小缩小了源代码的大小,因此其在网络上的传输变得更有效。

这对于 Web 和 移动应用程序 尤其有用,其中前端向后端发出http请求以获取文件,用户数据等资源。对于相当大的应用程序,如InstagramFacebook,前端通常安装在用户的设备上,而后端和数据库作为本地服务器或云中的多个实例存在。

在诸如加载照片的典型用户操作中,前端向后端发出http请求,而后端又向数据库实例发出请求以获取所请求的资源。这涉及通过网络传输数据,并且该过程的效率与正在传输的数据的大小成正比。这正是缩小有用的地方。

那怎么进行缩小呢?

在上一节中,我们看到了如何手动缩小一个简单的代码。但这对于巨大的代码库来说实际上不是一个可扩展的解决方案。多年来已经有各种构建工具来缩小 JavaScript 代码。接下来让我们来看看最受欢迎的一些解决方案:

UglifyJS

UglifyJS 的目标是缩小和压缩代码。让我们继续使用以下命令安装它:

npm install uglify - js - g

现在让我们尝试在JavaScript模块上运行uglify。为此,我编写了一个示例模块,代码如下:sample.js

var print = "Hello world! Let's minify everything because, less is more"

var apple = [1,2,3,4,5]

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

for(let element in apple) {
    console.log(print)
    console.log(element)
}

    

此代码基本上在循环内打印字符串和数组。我们多次复制for()循环以增加文件的大小,这样我们就可以更好地看到Uglify的效果。

文件大小为1.2KB

UglifyJS选项

我们可以看到,Uglify 有很多选项,其中大部分都是不言自明的。那么,让我们继续尝试其中几个:

文件大小为944B,并通过重复打印字符串和数组值来执行

我们在文件上使用了 -c(compress)和-m(mangle)选项并对其进行了修改。文件大小减少到944B,减少了大约22%。现在,让我们看看文件内容,看看它是如何通过uglification更改的:-c -m sample.js

var print="Hello world! Let's minify everything because, less is more",apple=[1,2,3,4,5];for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log    (print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);for(i=0;i<5;i++)console.log(print),console.log(apple[i]);

从上面的示例中,我们可以看到输出的内容具有相同的代码,没有任何空格和换行符。

为了进一步了解Uglify的效果,让我们用原型函数编写一个示例JS代码:

// comments 

function sample(helloworld) {
  this.hello = helloworld
}
sample.prototype.printvar = function()
{
  console.log(this.hello)
}


var hello = "hello world"
var s = new sample(hello)
s.printvar()
 
 

现在,让我们编译一下这段代码:

Uglified 代码是131B

请注意,我使用了Uglify (附带)选项。此选项将所有内容嵌入到一个大函数中,具有可配置的参数和值。为了理解这意味着什么,让我们看一下输出的内容:-e

!function(){function o(o){this.hello=o}o.prototype.printvar=function(){console.log(this.hello)};new o("hello world").printvar()}();


uglified输出中,我们可以看到函数名称 sample 已经消失,并且被替换为 o。所有代码都包含在一个大函数中,这会以可读性为代价进一步减小代码的大小。

现在,让我们看看另一个流行的缩小器:babel-minify

babel-minify(又名Babili)

babel- minify,以前称为Babili,是一个实验项目,试图使用Babel的工具链(用于编译)以类似的方式做一些事情:缩小。它目前是 0.x,官方存储库不建议在生产中使用它。

当我们已经拥有Uglify时,为什么我们需要这个工具?如果你注意到前面的例子,我没有使用最新版ECMAScript的语法。这是因为Uglify还不支持它 - 但是babel-minify可以。

这是因为它只是一组Babel插件,Babel已经了解了解析器Babylon的新语法。此外,当可以仅定位支持较新 ES 功能的浏览器时,您的代码大小可以更小,因为您不必进行转换然后缩小它。

babel-minify 之前,我们将运行Babel来转换ES6,然后运行Uglify来缩小代码。通过babel-minify,这个两步过程基本上变成了一个步骤。

babel-minify 是ES2015 +知道的,因为它是使用 Babel 工具链构建的。它被写成一组Babel插件,带有 babel-preset-minify 的消耗品。我们来看一个例子。

让我们使用以下命令在本地安装 BabelBabel 预设以转换ES6:

npm install - save - dev @babel / core @babel / cli
 npm install - save - dev babel - plugin - transform - es2015 - classes

现在,让我们用ES6语法编写一个:sample.js

//ES6 Syntax

class sample {
  constructor(helloworld) {
      this.hello = helloworld
  }
  printvar() {
    console.log(this.hello)
  }
}

var hello = "hello world"
var s = new sample(hello)
s.printvar()

让我们使用以下命令来转换此代码:

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

//ES6 Syntax
let sample = function () {
  function sample(helloworld) {
    _classCallCheck(this, sample);

    this.hello = helloworld;
  }

  _createClass(sample, [{
    key: "printvar",
    value: function printvar() {
      console.log(this.hello);
    }
  }]);

  return sample;
}();

var hello = "hello world";
var s = new sample(hello);
s.printvar();

已转换代码的内容如下所示:

如您所见,ES6 类语法已转换为常规函数语法。现在,让我们在这个内容上运行 Uglify 来缩小它:

uglifyjs sample-transpiled.js -c -m -e -o sample-transpiled-uglified.js

现在,压缩的内容如下所示:

function _classCallCheck(e,l){if(!(e instanceof l))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,l){for(var n=0;n<l.length;n++){var r=l[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,l,n){return l&&_defineProperties(e.prototype,l),n&&_defineProperties(e,n),e}let sample=function(){function e(l){_classCallCheck(this,e),this.hello=l}return _createClass(e,[{key:"printvar",value:function(){console.log(this.hello)}}]),e}();var hello="hello world",s=new sample(hello);s.printvar();

如果我们比较这些文件大小,sample.js is 227B, sample-transpiled.js is 1KB, and sample-transpiled-uglified.js is 609B. 很明显的发现,这不是最佳的处理过程,因为它会导致文件大小的增加。 为了解决这个问题,引入了 babel-minify。 现在,让我们安装 babel-minify 并尝试转换和缩小代码。

npm install babel-minify --save-dev

接下来我们使用下面的命令来压缩相同的 sample.js

minify sample.js > sample-babili.js

我们可以看下执行过后的输出内容:

class sample{constructor(l){this.hello=l}printvar(){console.log(this.hello)}}var hello="hello world",s=new sample(hello);s.printvar();

这个文件的大小是 135B, 将近压缩了40%,是一种更好的压缩代码的方式。它直接提高了通过网络传输它的效率,并可以在很多浏览器上面运行,因为 Babel 可以转换代码。还有各种插件可以使用。

Terser

TerserES6+JavaScript 解析器 和 mangler/compressor 工具包, 它可能是最有效的。Terser 建议您搭配 Rollup 打包使用,这样会产生更小的代码。

Rollup 是一个类似于 webpack 的 模块打包器,它是为了尽可能高效地构建 JavaScript 库的平面可分发版而创建的,利用了ES2015模块的巧妙设计。ES6 模块最终还是要由浏览器原生实现,但当前 Rollup 可以使你提前体验。

虽然 Rollup 是一个不错的选择,但如果你使用的是 webpack> v4,默认情况下会使用 Terser。 可以通过切换布尔变量来启用 Terser,如下所示:

module.exports = {
  //...
  optimization: {
    minimize: false
  }
};

安装 Terser

npm install terser -g

Terser 命令行具有以下语法:

terser [input files] [options]

Terser 可以采用多个输入文件。 建议您先传递输入文件,然后传递选项。 Terser 将按顺序解析输入文件并应用任何压缩选项。

这些文件在同一个全局范围内解析 - 也就是说,从文件到另一个文件中声明的某个变量/函数的引用将被正确匹配。 如果未指定输入文件,则 Terser 将从STDIN 读取。

如果您希望在输入文件之前传递选项,请使用双短划线将两者分开以防止输入文件用作选项参数:

terser --compress --mangle -- input.js

现在让我们尝试运行我们的sample.js代码:

terser -c toplevel,sequences=false --mangle -- sample.js > sample-terser.js

以下是此输出的内容

new class{constructor(l){this.hello=l}printvar(){console.log(this.hello)}}("hello world").printvar();

我们可以看到,到目前为止,我们所看到的所有工具中的输出是迄今为止最好的。 文件大小为 102B,比原始 sample.js 大小减少了近 55%。 可以使用 --help 选项找到 Terser 的其他命令行选项。

Terser 命令行选项。 在所有选项中,我们最感兴趣的是 --compress--mangle,每个选项都有自己的选项集。 --compress--mangle 的选项使您可以控制如何处理源代码以生成缩小的输出。

如果您注意到,我们已经在第一个 Terser 示例中使用了 --compress 的顶层和序列选项。 例如,您可以将 true 传递给 --compressdrop_console 选项以从源代码中删除所有 console.* 函数,如果您不想破坏类名,则可以使用 keep_classnames 选项。

有时,美化生成的输出可能很有用。 您可以使用 --beautify 选项执行此操作。 许多构建工具使用Terser - 在这里找到它们

让我们尝试在源文件中使用 drop_console 选项来查看console.log()函数是否被删除:

terser --compress drop_console=true -- sample.js > sample-drop-console.js

现在,让我们看看源代码,sample.js的内容:

//ES6 Syntax

class sample {
  constructor(helloworld) {
      this.hello = helloworld
  }
  printvar() {
    console.log(this.hello)
  }
}

var hello = "hello world"
var s = new sample(hello)
s.printvar()

而现在输出,sample-drop-console.js:

new class{constructor(r){this.hello=r}printvar(){}}("hello world").printvar();

正如我们所看到的,代码被进一步破坏和压缩,这个新文件的大小仅为 79B。 与不使用 drop_console选项时所看到的 55%相比,这减小了65%。 这样,我们可以根据项目的要求使用选项来平衡可读性和性能。

性能比较

到目前为止,我们共研究了三种最流行的压缩 js 和css 代码的工具。Babel 存储库比较基准测试结果提供了这些统计信息,可以帮助您为项目选择合适的压缩工具。

通过上面的图,我们可以看拿到,Terser 在基于 React项目中表现最好。您还可以在查看其他 web 框架的表现结果。

为 React 项目配置 minifiers

在本节中,我们将介绍为 React 应用程序配置 minifier的过程。 让我们在这个例子中使用Terser来缩小React应用程序。 为了以简化的方式实现这一目标,我们使用 webpackwebpack 是一个工具链,用于将所有文件捆绑到一个名为bundle.js的文件中,该文件可以高效加载。 让我们使用以下命令安装 webpack

npm install webpack --save-dev

我们还需要安装一些Babel插件以便转换代码:

npm install babel-core babel-loader babel-preset-env babel-preset-react\babel-preset-stage-0 --save-dev

接下来,让我们安装Terser插件:

npm install terser-webpack-plugin --save-dev

我们还需要.svg.css的 loader,所以我们也要安装它们:

npm install svg-inline-loader --save-dev
npm install css-loader --save-dev

在这个阶段,我们只需要配置 webpack.config.js 文件,具体配置可以参考下面:

const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development',
  module: {
    rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules)/,
      use: {
        loader: 'babel-loader',
        options:{
          presets: ['@babel/preset-react']
        }
      }
    },
    {
      test: /\.svg$/,
      loader: 'svg-inline-loader'
    },
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader'],
    }]
  },
  optimization: {
    minimizer: [new TerserPlugin()],
  },
}

从上面的代码中,我们可以看到webpack的入口文件是src /中的 index.js,最终输出将作为bundle.js存储在dist /目录中。 优化字段将 TerserPlugin拉入缩小过程。 现在,让我们运行webpack来静态构建我们的应用程序以进行生产。

webpack的输出。

我们可以看到 webpack 在所有文件上运行 loader 和插件,并构建了一个大小为 938KBbundle.js,而我们的整个应用程序比这大得多。 这是webpack以及相关加载器和插件的真正威力。

最近推出了一些新的捆绑包。 其中,RollupParcel越来越受欢迎。 任何捆绑工具的基础配置和设置类似于 webpack。 您可以在此处找到 webpackRollupParcel之间的性能比较, 链接在此

结论

最后一点,让我通过展示npm趋势的片段来结束这篇文章。 我们可以看到,Terser 在过去六个月中获得了极大的人气。 与其他缩小工具相比,这可以直接归因于其更好的性能。

感谢您阅读这篇文章。 我希望你能学习到压缩代码的一些知识和对一些长期的疑惑点能够得到清晰地解决。