JS 压缩 Terser

Terser 是一个 JavaScript 的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集

早期会使用 uglify-js 来压缩、丑化 JavaScript 代码,但是目前已经不再维护,并且不支持 ES6+的语法

Terser 是从 uglify-es fork 过来的,并且保留它原来的大部分 API 以及适配 uglify-es 和 uglify-js@3 等

也就是说,Terser 可以帮助我们压缩、丑化我们的代码,让我们的 bundle 变得更小。

Terser 是一个独立的工具,所以它可以单独安装

npm install terser

命令行使用

1
2
3
4
terser [input files] [options]

# 举例说明
terser js/file1.js -o foo.min.js -c -m

对应 Compress option 和 Mangle option

Compress 和 Mangle 的 options

Compress option:

  • arrows:class 或者 object 中的函数,转换成箭头函数
  • arguments:将函数中使用 arguments[index]转成对应的形参名称
  • dead_code:移除不可达的代码(tree shaking)

其他属性可以查看文档;

Mangle option

  • toplevel:默认值是 false,顶层作用域中的变量名称,进行丑化(转换)
  • keep_classnames:默认值是 false,是否保持依赖的类名称
  • keep_fnames:默认值是 false,是否保持原来的函数名称
  • 其他属性可以查看文档
1
2
3
npx terser ./src/abc.js -o abc.min.js -c
arrows,arguments=true,dead_code -m
toplevel=true,keep_classnames=true,keep_fnames=true

Terser 在 webpack 中配置

真实开发中,不需要手动的通过 terser 来处理代码,可以直接通过 webpack 来处理:

在 webpack 中有一个 minimizer 属性,在 production 模式下,默认就是使用 TerserPlugin 来处理我们的代码的

如果对默认的配置不满意,也可以自己来创建 TerserPlugin 的实例,并且覆盖相关的配置;

  • 首先,打开 minimize,对代码进行压缩(默认 production 模式下已经打开了)
  • 其次,在 minimizer 创建一个 TerserPlugin:
    • extractComments:默认值为 true,表示会将注释抽取到一个单独的文件中;
    • 在开发中,我们不希望保留这个注释时,可以设置为 false;
  • parallel:使用多进程并发运行提高构建的速度,默认值是 true,并发运行的默认数量: os.cpus().length - 1;
    • 我们也可以设置自己的个数,但是使用默认值即可;
  • terserOptions:设置我们的 terser 相关的配置
    • compress:设置压缩相关的选项;
    • mangle:设置丑化相关的选项,可以直接设置为 true;
      • toplevel:底层变量是否进行转换;
      • keep_classnames:保留类的名称;
      • keep_fnames:保留函数的名称;

Css 压缩

CSS 压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;

CSS 的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin;

css-minimizer-webpack-plugin 是使用 cssnano 工具来优化、压缩 CSS(也可以单独使用)

1
npm install css-minimizer-webpack-plugin -D
1
2
3
4
5
6
7
minimizer: [
new TerserPlugin({}),
new CssMinimizerPlugin({
// 默认就有这个配置
// parallel:true
}),
];

Tree Shaking

Tree Shaking 是一个术语,在计算机中表示消除死代码(dead_code)

最早的想法起源于 LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求在进行函数式编程时,尽量使用纯函数的原因之一)

后来 Tree Shaking 也被应用于其他的语言,比如 JavaScript、Dart;

JavaScript 的 Tree Shaking:

  • 对 JavaScript 进行 Tree Shaking 是源自打包工具 rollup(
  • 因为 Tree Shaking 依赖于 ES Module 的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)
  • webpack2 正式内置支持了 ES2015 模块,和检测未使用模块的能力
  • webpack4 正式扩展了这个能力,并且通过 package.json 的 sideEffects 属性作为标记,告知 webpack 在编译时,哪里文件可以安全的删除掉
  • pwebpack5 中,也提供了对部分 CommonJS 的 tree shaking 的支持

https://github.com/webpack/changelog-v5#commonjs-tree-shaking

webpack 实现 Tree Shaking

webpack 实现 Tree Shaking 采用了两种不同的方案:

  • usedExports:通过标记某些函数是否被使用,之后通过 Terser 来进行优化的
  • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用

usedExports

将 mode 设置为 development 模式:

  • 为了可以看到 usedExports 带来的效果,我们需要设置为 development 模式
  • 因为在 production 模式下,webpack 默认的一些优化会带来很大额影响。
  • production 下默认为 true

设置 usedExports 为 true 和 false 对比打包后的代码:

  • 在 usedExports 设置为 true 时,会有一段注释:unused harmony export mul
  • 这段注释告知 Terser 在优化时,可以删除掉这段代码

这个时候, minimize 设置 true:

  • usedExports 设置为 false 时,mul 函数没有被移除掉
  • usedExports 设置为 true 时,mul 函数有被移除掉
  • 所以,usedExports 实现 Tree Shaking 是结合 Terser 来完成的

sideEffects

sideEffects 用于告知 webpack compiler 哪些模块时有副作用的

副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过 export 来判断这段代码的意义;

在 package.json 中设置 sideEffects 的值:

  • 将 sideEffects 设置为 false,就是告知 webpack 可以安全的删除未用到的 exports
  • 如果有一些我们希望保留,可以设置为数组;
    • 比如我们有一个 format.js、style.css 文件:
    • 该文件在导入时没有使用任何的变量来接受
1
import "./index.css";

那么打包后的文件,符合这两个条件的文件不会被优化;

Scope Hoisting 作用于提升

Scope Hoisting 从 webpack3 开始增加的一个新功能

  • 功能是对作用域进行提升,并且让 webpack 打包后的代码更小、运行更快
  • 默认情况下 webpack 打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:
  • 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
  • Scope Hoisting 可以将函数合并到一个模块中来运行

使用 Scope Hoisting 非常的简单,webpack 已经内置了对应的模块

  • 在 production 模式下,默认这个模块就会启用
  • 在 development 模式下,我们需要自己来打开该模块
1
2
3
const webpack = require("webpack");

plugins: [new webpack.optimize.ModuleConcatenationPlugin()];

HTTP 压缩

HTTP 压缩是一种内置在 服务器 和 客户端之间的,以改进传输速度和带宽利用率的方式;

  • 第一步:HTTP 数据在服务器发送前就已经被压缩了;(可以在 webpack 中完成)
  • 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式
  • 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器

压缩格式

  • compress – UNIX 的“compress”程序的方法(历史性原因,不推荐大多数应用使用,应该使用 gzip 或 deflate)
  • deflate – 基于 deflate 算法(定义于 RFC 1951)的压缩,使用 zlib 数据格式封装
  • gzip – GNU zip 格式(定义于 RFC 1952),是目前使用比较广泛的压缩算法
  • br – 一种新的开源压缩算法,专为 HTTP 内容的编码而设计

Webpack 对文件压缩

使用 CompressionPlugin

1
npm install compression-webpack-plugin -D

HTML 压缩

HtmlWebpackPlugin 插件来生成 HTML 的模板,事实上它还有一些其他的配置

  • inject:设置打包的资源插入的位置 true、 false 、body、head
  • cache:设置为 true,只有当文件改变时,才会生成新的文件(默认值也是 true)
  • minify:默认会使用一个插件 html-minifier-terser

打包后模块时间分析

pnpm add speed-measure-webpack-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const speedMeasureWebpackPlugin = require("speed-measure-webpack-plugin");
const smp = new speedMeasureWebpackPlugin();
const config = {
// watch: true,
mode: "development",
entry: { index: "./src/index.js" },
output: {
// 打包后的文件的输出目录
path: path.resolve(__dirname, "./build"),
//
filename: "[name]-bundle.js",
// 可以对动态导出的包进行自定一命名
chunkFilename: "[id]-[name]-chunk.js",
},
resolve: {
extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
alias: {
"@": path.resolve(__dirname, "./src"),
pages: path.resolve(__dirname, "./src/pages"),
},
},
optimization: {
chunkIds: "deterministic",
splitChunks: {
chunks: "all",
maxSize: 20000,
minSize: 10,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
filename: "[name]_vendors.js",
},
utils: {
test: /utils/,
filename: "[name]_utils.js",
},
},
},
},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
}),
// new ReactRefreshWebpackPlugin(),
// new VueLoaderPlugin(),
],
};
smp.wrap(config);
module.exports = config;

打包后模块大小分析

webpack-bundle-analyzer