webpack中TreeShaking及压缩的实践

# tree-shaking

# 原理

  1. ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码。
  2. 分析程序流,判断哪些变量未被使用、引用,进而删除此代码。

# 什么是死代码

很简单:就是 Webpack 没看到你使用的代码。Webpack 跟踪整个应用程序的 import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那是“死代码”,并会对其进行 tree-shaking 。

死代码并不总是那么明确的。下面是一些死代码和“活”代码的例子,希望能让你更明白。请记住,在某些情况下,Webpack 会将某些东西视为死代码,尽管它实际上并不是;

// 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
// 这会被看作“活”代码,不会做 tree-shaking
import Stuff from './stuff';
doSomething(Stuff);
// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import Stuff from './stuff';
doSomething();
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import './stuff';
doSomething();
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import 'my-lib';
doSomething();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 支持 tree-shaking写 import

# 用支持 tree-shaking 的方式写 import

在编写支持 tree-shaking 的代码时,导入方式非常重要。你应该避免将整个库导入到单个 JavaScript 对象中。当你这样做时,你是在告诉 Webpack 你需要整个库, Webpack 就不会摇它。

以流行的库 Lodash 为例。一次导入整个库是一个很大的错误,但是导入单个的模块要好得多。当然,Lodash 还需要其他的步骤来做 tree-shaking,但这是个很好的起点。

// 全部导入 (不支持 tree-shaking)
import _ from 'lodash';
// 具名导入(支持 tree-shaking)
import { debounce } from 'lodash';
// 直接导入具体的模块 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';
1
2
3
4
5
6

# 基本的 Webpack 配置

使用 Webpack 进行 tree-shaking 的第一步是编写 Webpack 配置文件。你可以对你的 webpack 做很多自定义配置,但是如果你想要对代码进行 tree-shaking,就需要以下几项。

  • 首先,你必须处于生产模式。Webpack 只有在压缩代码的时候会 tree-shaking,而这只会发生在生产模式中。
  • 其次,必须将优化选项 “usedExports” 设置为true。这意味着 Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。
  • 最后,你需要使用一个支持删除死代码的压缩器。这种压缩器将识别出 Webpack 是如何标记它认为没有被使用的代码,并将其剥离。TerserPlugin 支持这个功能,推荐使用。

下面是 Webpack 开启 tree-shaking 的基本配置:

// Base Webpack Config for Tree Shaking
const config = {
 mode: 'production',
 optimization: {
  usedExports: true,
  minimizer: [
   new TerserPlugin({...})
  ]
 }
};
1
2
3
4
5
6
7
8
9
10

# 告诉 Webpack无副作用

package.json 有一个特殊的属性 sideEffects,就是为此而存在的。它有三个可能的值:

true 是默认值,如果不指定其他值的话。这意味着所有的文件都有副作用,也就是没有一个文件可以 tree-shaking。

false 告诉 Webpack 没有文件有副作用,所有文件都可以 tree-shaking。

第三个值 […] 是文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。因此,除了指定的文件之外,其他文件都可以安全地进行 tree-shaking。

每个项目都必须将 sideEffects 属性设置为 false 或文件路径数组。在我公司的工作中,我们的基本应用程序和我提到的所有共享库都需要正确配置 sideEffects 标志。

下面是 sideEffects 标志的一些代码示例。尽管有 JavaScript 注释,但这是 JSON 代码:

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 全局 CSS 与副作用

关于对于csstree shake,建议是purecss对没有使用的css代码剔除

首先,让我们在这个上下文中定义全局 CSS。全局 CSS 是直接导入到 JavaScript 文件中的样式表(可以是CSS、SCSS等)。它没有被转换成 CSS 模块或任何类似的东西。基本上,import 语句是这样的:

// 导入全局 CSS
import './MyStylesheet.css';
1
2

因此,如果你做了上面提到的副作用更改,那么在运行 webpack 构建时,你将立即注意到一个棘手的问题。以上述方式导入的任何样式表现在都将从输出中删除。这是因为这样的导入被 webpack 视为死代码,并被删除。

幸运的是,有一个简单的解决方案可以解决这个问题。Webpack 使用它的模块规则系统来控制各种类型文件的加载。每种文件类型的每个规则都有自己的 sideEffects 标志。这会覆盖之前为匹配规则的文件设置的所有 sideEffects 标志。

所以,为了保留全局 CSS 文件,我们只需要设置这个特殊的 sideEffects 标志为 true,就像这样:

// 全局 CSS 副作用规则相关的 Webpack 配置
const config = {
 module: {
  rules: [
   {
    test: /regex/,
    use: [loaders],
    sideEffects: true
   }
  ]
 } 
};
1
2
3
4
5
6
7
8
9
10
11
12

Webpack 的所有模块规则上都有这个属性。处理全局样式表的规则必须用上它,包括但不限于 CSS/SCSS/LESS/等等。

# es2015 模块 Babel 配置

据我所知,Babel 不支持将其他模块系统编译成 es2015 模块。但是,如果你是前端开发人员,那么你可能已经在使用 es2015 模块编写代码了,因为这是全面推荐的方法。

因此,为了让我们编译的代码使用 es2015 模块,我们需要做的就是告诉 babel 不要管它们。为了实现这一点,我们只需将以下内容添加到我们的 babel.config.js 中(在本文中,你会看到我更喜欢JavaScript 配置而不是 JSON 配置):

// es2015 模块的基本 Babel 配置
const config = {
 presets: [
  [
   '[@babel/preset-env](http://twitter.com/babel/preset-env)',
   {
    modules: false
   }
  ]
 ]
};
1
2
3
4
5
6
7
8
9
10
11

modules 设置为 false,就是告诉 babel 不要编译模块代码。这会让 Babel 保留我们现有的 es2015 import/export 语句。

# 总括配置

  • 如果使用Babel的话,在.babelrc文件中就是在webpack.config.js文件中设置modules: false就好;

  • 需要使用UglifyJsPlugin插件。如果在mode:"production"模式,这个插件已经默认添加了,如果在其它模式下,可以手工添加它。TerserPlugin也可以;

    另外要记住的是打开optimization.usedExports。**在mode: "production"模式下,它也是默认打开了的。**它告诉webpack每个模块明确使用exports。这样之后,webpack会在打包文件中添加诸如/* unused harmony export */这样的注释,其后UglifyJsPlugin插件会对这些注释作出理解。

    Webpack默认忽略了sideEffect标注,改变此行为需要设置optimization.sideEffects为true。你能手工设置它或通过设置mode:"production"模式也行。

  • 必须使用ES6模块,不能使用其它类型的模块如CommonJS之流;不做全局引入处理;

第一步:

// .babelrc
{
  "presets": [
    ["env",
      {
        "modules": false
      }
    ]
  ]
}

// webpack.config.js
module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /(node_modules)/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['env', { modules: false }]
                }
            }
        }
    ]
},
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

第二步:

// webpack.config.js
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const UglifyJS = require('uglify-es');

const DefaultUglifyJsOptions = UglifyJS.default_options();
const compress = DefaultUglifyJsOptions.compress;
for(let compressOption in compress) {
    compress[compressOption] = false;
}
compress.unused = true;

module.exports = {
    mode: 'none',
    optimization: {
        minimize: true,
        minimizer: [
            new UglifyJsPlugin({
                uglifyOptions: {
                    compress,
                    mangle: false,
                    output: {
                        beautify: true
                    }
                },
            })
        ],
    }
}
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

Here is some code examples of the sideEffects flag. Despite the JavaScript comments, this is JSON code:

// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'none',
   // plugins:[
   //     new webpack.optimize.UglifyJsPlugin(),
   //     new webpack.HotModuleReplacementPlugin()
   // ],
    optimization: {
        minimize: true,
        usedExports: true,
        sideEffects: true
        minimizer: [
            new UglifyJsPlugin()//new TerserPlugin({...})
        ],
    },
    plugins: [
        new HtmlWebpackPlugin()
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// All files have side effects, and none can be tree-shaken
{
 "sideEffects": true
}
// No files have side effects, all can be tree-shaken
{
 "sideEffects": false
}
// Only these files have side effects, all other files can be tree-shaken, but these must be kept
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第三步:

/import _ from 'lodash';
//console.log(_.debounce);

import { debounce } from 'lodash';// (译注:原文如此,应该为'lodash-es')
console.log(debounce);

// Import everything (NOT TREE-SHAKABLE)
import _ from 'lodash';
// Import named export (CAN BE TREE SHAKEN)
import { debounce } from 'lodash';
// Import the item directly (CAN BE TREE SHAKEN)
import debounce from 'lodash/lib/debounce';
1
2
3
4
5
6
7
8
9
10
11
12

# 压缩及移除打印

# 相关链接

How to Fully Optimize Webpack 4 Tree Shaking | by Craig Miller | Medium (opens new window)

上次更新: 2022/04/15, 05:41:27
×