// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import Stuff from ‘./stuff’;
doSomething();
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import ‘./stuff’;
doSomething();
// 全部导入 (不支持 tree-shaking)
import _ from ‘lodash’;
// 具名导入(支持 tree-shaking)
import { debounce } from ‘lodash’;
// 直接导入具体的模块 (支持 tree-shaking)
import debounce from ‘lodash/lib/debounce’;
// 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
// 这会被看作“活”代码,不会做 tree-shaking
import Stuff from ‘./stuff’;
doSomething(Stuff);
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import ‘my-lib’;
export { default as Title } from ‘./Title’;
export { default as Options } from ‘./Options’;
export { default as AddonArea } from ‘./AddonArea’;
export { default as Answer } from ‘./AddonArea/Answer’;
export { default as Analysis } from ‘./AddonArea/Analysis’;
export { default as OriginalText } from ‘./AddonArea/OriginalText’;
export { default as Labels } from ‘./AddonArea/Labels’;
这样的文件结构是无法进行 tree-shaking 的, 因为没有 import?!
自执行的模块 import
自执行模块我们通常会使用 import 'xxx'
来进行模块引用,而不进行显式的调用。因此模块本身就有副作用。
import ‘utils/refresh’
对于这种模块可以这样处理:
-
在 sideEffects 中通过数组声明,使其在 Tree Shaking 的范围之外
-
模块改造,暴露成员支持显式调用
unused harmony export
如果该模块被标识为 unused harmony export,则说明没有外部引用使用到该成员,webpack 认为是可以安全去除的。
harmony export
部分被标识为 harmony export 的模块也会被去除。这个是跟 UglifyJS 的机制有关系。
没有提供导出成员的模块
// ./src/modules/edu-discount/seckill/index.ts
import * as SeckillTypes from ‘./types’;
export { SeckillTypes };
对于只有暴露的成员,但是没有被引用的成员,这种模块会被直接删除。
-
[x] exports provided
-
[ ] exports used
配置
–
babel的配置文件
{
“presets”: [
[“env”, {
“modules”: false // 配置了这个,babel就不会像默认那样转变成 require 形式。
}],
“stage-2”,
“react”
]
}
为 webpack 进行 tree-shaking 创造了条件。
⚠️不能引用类似 @babel/plugin-transform-modules-commonjs
会把模块编译成 commonjs 的插件;
webpack 的配置文件
webpack 4 通过 optimization 取代了4个常用的插件:
| 废弃插件 | optimization 属性 | 功能 |
|
| — | — | — | — |
| UglifyjsWebpackPlugin | sideEffects | minimizer | Tree Shaking & Minimize |
| ModuleConcatenationPlugin | concatenateModules | Scope hoisting | 生产环境默认开启 |
| CommonsChunkPlugin | splitChunks | runtimeChunk | OccurrenceOrder |
| NoEmitOnErrorsPlugin | NoEmitOnErrors | 编译出现错误时,跳过输出阶段 | 生产环境默认开启 |
usedExports
Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。
// Base Webpack Config for Tree Shaking
const config = {
mode: ‘production’,
optimization: {
usedExports: true,
minimizer: [
new TerserPlugin({…}) // 支持删除死代码的压缩器
]
}
};
package.json 的配置
用过 redux 的童鞋应该对纯函数不陌生,自然也就应该了解函数式编程,函数式编程中就有副作用一说。
照顾一下不知道的同学,那什么是副作用呢?
一个函数会、或者可能会对函数外部变量产生影响的行为。
具有副作用的文件不应该做 tree-shaking,因为这将破坏整个应用程序。比如全局样式表及全局的 JS 配置文件。
webpack 总会害怕把你要用的代码删除了,所以默认所有的文件都有副作用,不能被 Tree Shaking。
// 所有文件都有副作用,全都不可 tree-shaking
{
“sideEffects”: true
}
// 没有文件有副作用,全都可以 tree-shaking,即告知 webpack,它可以安全地删除未用到的 export。
{
“sideEffects”: false
}
// 除了数组中包含的文件外有副作用,所有其他文件都可以 tree-shaking,但会保留符合数组中条件的文件
{
“sideEffects”: [
“*.css”,
“*.less”
]
}
所以,首先关闭你的 sideEffects,
直接通过 module.rules
中的 sideEffects 配置可缩小你的影响范围。
加了 sideEffect 配置后,构建出来的一些 IIFE 函数也会加上/PURE/注释,便于后续 treeshaking。
组件不支持DCE?
我们的组件用的是 father,可以看到其依赖的father-build 是基于 rollup 的,那就好办了。webpack 的 Tree Shaking 还是 copy 的 rollup家的。
关键是在应用组件的业务项目里面配置optimization.sideEffects: true
// webpack.config.js
const path = require(‘path’)
const webpackConfig = {
module : {
rules: [
{
test: /.(jsx|js)$/,
use: ‘babel-loader’,
exclude: path.resolve(__dirname, ‘node_modules’)
}
]
},
optimization : {
sideEffects: true,
minimizer: [
// 这里配置成空数组是为了使最终产生的 main.js 不被压缩
]
},
plugins:[]
};
module.exports = webpackConfig;
// package.json
{
“name”: “treeshaking-test”,
“version”: “0.1.0”,
“description”: “”,
“main”: “src/index.js”,
“scripts”: {
“build”: “webpack --config webpack.config.js”
},
“author”: “lu.lu lulu27753@163.com (https://github.com/lulu27753)”,
“license”: “MIT”,
“dependencies”: {
“big-module”: “^0.1.0”,
“big-module-with-flag”: “^0.1.0”,
“webpack-bundle-analyzer”: “^3.7.0”
},
“devDependencies”: {
“babel-preset-env”: “^1.7.0”,
“webpack”: “^4.43.0”,
“webpack-cli”: “^3.3.11”
}
}
// .babelrc
{
“presets”: [
[“env”, { “modules”: false }]
]
}
可以看到最终打包后的文件如下:
// dist/main.js
“use strict”;
// ESM COMPAT FLAG
webpack_require.r(webpack_exports);
// CONCATENATED MODULE: ./node_modules/big-module/es/a.js
var a = ‘a’;
// CONCATENATED MODULE: ./node_modules/big-module/es/b.js
var b = ‘b’;
// CONCATENATED MODULE: ./node_modules/big-module/es/c.js
var c = ‘c’;
// CONCATENATED MODULE: ./node_modules/big-module/es/index.js
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/a.js
var a_a = ‘a’;
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/b.js
var b_b = ‘b’;
// CONCATENATED MODULE: ./src/index.js
console.log(a, b, a_a, b_b);
/***/ })
/******/ ]);
可以很清楚的看到 big-module-with-flag
中的 c 模块被DCE
了。
做个小小的改动,将 .babelrc
中的 modules
改为"commonjs"
{
“presets”: [
[“env”, { “modules”: “commonjs” }]
]
}
“use strict”;
// ESM COMPAT FLAG
webpack_require.r(webpack_exports);
// EXPORTS
webpack_require.d(webpack_exports, “a”, function() { return /* reexport */ a; });
webpack_require.d(webpack_exports, “b”, function() { return /* reexport */ b; });
webpack_require.d(webpack_exports, “c”, function() { return /* reexport */ c; });
// CONCATENATED MODULE: ./node_modules/big-module/es/a.js
var a = ‘a’;
// CONCATENATED MODULE: ./node_modules/big-module/es/b.js
var b = ‘b’;
// CONCATENATED MODULE: ./node_modules/big-module/es/c.js
var c = ‘c’;
// CONCATENATED MODULE: ./node_modules/big-module/es/index.js
/***/ }),
/* 2 */
/***/ (function(module, webpack_exports, webpack_require) {
“use strict”;
// ESM COMPAT FLAG
webpack_require.r(webpack_exports);
// EXPORTS
webpack_require.d(webpack_exports, “a”, function() { return /* reexport */ a; });
webpack_require.d(webpack_exports, “b”, function() { return /* reexport */ b; });
webpack_require.d(webpack_exports, “c”, function() { return /* reexport */ c; });
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/a.js
var a = ‘a’;
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/b.js
var b = ‘b’;
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/c.js
var c = ‘c’;
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/index.js
/***/ })
/******/ ]);
结果是 CDE
失败!
将 modules
的值改回去,并升级big-module-with-flag
为0.2.0。CDE
成功,可以打假一波了(???,网上很多文章都是基于webpack3的,过时了)
升级big-module-with-flag
为0.5.0, 并更改 src/index.js
import { a as a1, b as b1 } from “big-module”;
import { a as a2, b as b2, Apple } from “big-module-with-flag”;
console.log(a1, b1, a2, b2);
const appleModel = new Apple({model: ‘IphoneX’}).getModel()
console.log(appleModel)
var Apple = /#PURE/function () {
function Apple(_ref) {
var model = _ref.model;
_classCallCheck(this, Apple);
this.className = ‘Apple’;
this.model = model;
}
_createClass(Apple, [{
key: “getModel”,
value: function getModel() {
return this.model;
}
}]);
return Apple;
}();
// CONCATENATED MODULE: ./src/index.js
console.log(a, b, es_a, es_b);
var appleModel = new Apple({
model: ‘IphoneX’
}).getModel();
console.log(appleModel);
DCE 成功!
var _bigModule = webpack_require(2);
var _bigModuleWithFlag = webpack_require(1);
console.log(_bigModule.a, _bigModule.b, _bigModuleWithFlag.a, _bigModuleWithFlag.b);
var appleModel = new _bigModuleWithFlag.Apple({
model: ‘IphoneX’
}).getModel();
console.log(appleModel);
/***/ }),
/* 1 */
/***/ (function(module, webpack_exports, webpack_require) {
“use strict”;
// ESM COMPAT FLAG
webpack_require.r(webpack_exports);
// EXPORTS
webpack_require.d(webpack_exports, “a”, function() { return /* reexport */ es_a; });
webpack_require.d(webpack_exports, “b”, function() { return /* reexport */ es_b; });
webpack_require.d(webpack_exports, “c”, function() { return /* reexport */ es_c; });
webpack_require.d(webpack_exports, “Person”, function() { return /* reexport */ Person; });
webpack_require.d(webpack_exports, “Apple”, function() { return /* reexport */ Apple; });
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/a.js
var a = ‘a’;
/* harmony default export */ var es_a = (a);
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/b.js
var b = ‘b’;
/* harmony default export */ var es_b = (b);
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/c.js
var c = ‘c’;
/* harmony default export */ var es_c = ©;
// CONCATENATED MODULE: ./node_modules/big-module-with-flag/es/Person.js
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); } }