概念
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。
在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。
原理
DCE:dead code elimination
- 代码不会被执行,不可到达
if (false) {
console.log('这段代码永远不会执行')
}
- 代码执行结果不会被用到
- 代码只会影响死变量(只写不读)
// 定义一个变量
const a = 123
// 但是没用地方去使用a
传统编译型的语言中,都是由编译器将Dead Code从AST(抽象语法树)中删除,那javascript中是由谁做DCE呢?代码压缩优化工具uglify。
Tree shaking 是 DCE 的一种新的实现,是一个通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code) 行为的术语。
Tree-shaking 和传统的 DCE的方法又不太一样,传统的DCE 消灭不可能执行的代码,而Tree-shaking 更关注于消除没有用到的代码。
tree-shaking的消除原理是依赖于ES6的模块特性。它依赖于ES6中的 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。
利用ES6模块的特点:
- 只能作为模块的顶层语句出现
- import的模块名只能是字符串常量
- import binding是immutable(永恒不变)的——依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,然后进行消除
实践
webpack4.0在mode:production时默认开启。
举1个栗子:
./src/math.js
export function square(x) {
return 'square is:' + x * x;
}
export function cube(x) {
return 'cube is:' + x * x * x;
}
./src/index.js
import { cube, square } from './math.js';
if (false) {
let a = square(5)
}
function component() {
let a = square(5)
const element = document.createElement('div');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
return element;
}
document.body.appendChild(component());
./webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'development',
devtool: 'inline-source-map'
};
./dist/bundle.js
/***/ "./src/math.js":
/*!*********************!*\
!*** ./src/math.js ***!
\*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "square": () => (/* binding */ square),
/* harmony export */ "cube": () => (/* binding */ cube)
/* harmony export */ });
function square(x) {
return 'square is:' + x * x;
}
function cube(x) {
return 'cube is:' + x * x * x;
}
/***/ })
mode=production:
./dist/bundle.js
(()=>{"use strict";document.body.appendChild(function(){const e=document.createElement("div");return e.innerHTML=["Hello webpack!","5 cubed is equal to "+(n=5,"cube is:"+n*n*n)].join("\n\n"),e;var n}())})();
将文件标记为 side-effect-free(无副作用)
在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。
通过 package.json 的 “sideEffects” 属性,来实现这种方式。
如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export。
再举1个栗子:
./src/side-effect.js
window.title = 'this has side effect'
./src/index.js
import './side-effect'
在package.json中设置sideEffects为false,则被认为未使用内部export的变量,可以删除side-effect.js
./package.json
{
"name": "tree-shaking",
...
"sideEffects": false,
"devDependencies": {
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0"
}
}
./dist/bundle.js
(()=>{"use strict";document.body.appendChild(function(){const e=document.createElement("div");return e.innerHTML=["Hello webpack!","5 cubed is equal to "+(n=5,"cube is:"+n*n*n)].join("\n\n"),e;var n}())})();
改为不设置sideEffects,或设置sideEffects为数组,告知有副作用
./package.json
{
"name": "tree-shaking",
...
"sideEffects": [
"./src/side-effect.js" // 若为数组,则表示除了数组中的模块,其余都没有副作用,可以放心删除
],
"devDependencies": {
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0"
}
}
则打包的文件中会包含对应代码
./dist/bundle.js
(()=>{var e={314:()=>{window.title="this has side effect"}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}(()=>{"use strict";n(314),document.body.appendChild(function(){const e=document.createElement("div");return e.innerHTML=["Hello webpack!","5 cubed is equal to "+(t=5,"cube is:"+t*t*t)].join("\n\n"),e;var t}())})()})();
注意
在使用 tree shaking 时必须有 ModuleConcatenationPlugin 的支持,您可以通过设置配置项 mode: “production” 以启用它。如果您没有如此做,请记得手动引入 ModuleConcatenationPlugin。
new webpack.optimize.ModuleConcatenationPlugin()