webpack中TreeShaking及压缩的实践
# tree-shaking
# 原理
- ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码。
- 分析程序流,判断哪些变量未被使用、引用,进而删除此代码。
# 什么是死代码
很简单:就是 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();
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';
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({...})
]
}
};
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"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 全局 CSS 与副作用
关于对于
css
的tree shake
,建议是purecss
对没有使用的css
代码剔除
首先,让我们在这个上下文中定义全局 CSS。全局 CSS 是直接导入到 JavaScript 文件中的样式表(可以是CSS、SCSS等)。它没有被转换成 CSS 模块或任何类似的东西。基本上,import 语句是这样的:
// 导入全局 CSS
import './MyStylesheet.css';
2
因此,如果你做了上面提到的副作用更改,那么在运行 webpack 构建时,你将立即注意到一个棘手的问题。以上述方式导入的任何样式表现在都将从输出中删除。这是因为这样的导入被 webpack 视为死代码,并被删除。
幸运的是,有一个简单的解决方案可以解决这个问题。Webpack 使用它的模块规则系统来控制各种类型文件的加载。每种文件类型的每个规则都有自己的 sideEffects
标志。这会覆盖之前为匹配规则的文件设置的所有 sideEffects
标志。
所以,为了保留全局 CSS 文件,我们只需要设置这个特殊的 sideEffects
标志为 true
,就像这样:
// 全局 CSS 副作用规则相关的 Webpack 配置
const config = {
module: {
rules: [
{
test: /regex/,
use: [loaders],
sideEffects: true
}
]
}
};
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
}
]
]
};
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 }]
}
}
}
]
},
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
}
},
})
],
}
}
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()
]
}
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"
]
}
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';
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)