概念

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。

原理

DCE:dead code elimination

  1. 代码不会被执行,不可到达
  1. if (false) {
  2. console.log('这段代码永远不会执行')
  3. }
  1. 代码执行结果不会被用到
  2. 代码只会影响死变量(只写不读)
  1. // 定义一个变量
  2. const a = 123
  3. // 但是没用地方去使用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模块的特点:

  1. 只能作为模块的顶层语句出现
  2. import的模块名只能是字符串常量
  3. import binding是immutable(永恒不变)的——依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,然后进行消除

实践

webpack4.0在mode:production时默认开启。

举1个栗子:

./src/math.js

  1. export function square(x) {
  2. return 'square is:' + x * x;
  3. }
  4. export function cube(x) {
  5. return 'cube is:' + x * x * x;
  6. }

./src/index.js

  1. import { cube, square } from './math.js';
  2. if (false) {
  3. let a = square(5)
  4. }
  5. function component() {
  6. let a = square(5)
  7. const element = document.createElement('div');
  8. element.innerHTML = [
  9. 'Hello webpack!',
  10. '5 cubed is equal to ' + cube(5)
  11. ].join('\n\n');
  12. return element;
  13. }
  14. document.body.appendChild(component());

./webpack.config.js

  1. const path = require('path');
  2. module.exports = {
  3. entry: './src/index.js',
  4. output: {
  5. filename: 'bundle.js',
  6. path: path.resolve(__dirname, 'dist'),
  7. },
  8. mode: 'development',
  9. devtool: 'inline-source-map'
  10. };

./dist/bundle.js

  1. /***/ "./src/math.js":
  2. /*!*********************!*\
  3. !*** ./src/math.js ***!
  4. \*********************/
  5. /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  6. __webpack_require__.r(__webpack_exports__);
  7. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  8. /* harmony export */ "square": () => (/* binding */ square),
  9. /* harmony export */ "cube": () => (/* binding */ cube)
  10. /* harmony export */ });
  11. function square(x) {
  12. return 'square is:' + x * x;
  13. }
  14. function cube(x) {
  15. return 'cube is:' + x * x * x;
  16. }
  17. /***/ })

mode=production:

./dist/bundle.js

  1. (()=>{"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

  1. window.title = 'this has side effect'

./src/index.js

  1. import './side-effect'

在package.json中设置sideEffects为false,则被认为未使用内部export的变量,可以删除side-effect.js

./package.json

  1. {
  2. "name": "tree-shaking",
  3. ...
  4. "sideEffects": false,
  5. "devDependencies": {
  6. "webpack": "^5.38.1",
  7. "webpack-cli": "^4.7.0"
  8. }
  9. }

./dist/bundle.js

  1. (()=>{"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

  1. {
  2. "name": "tree-shaking",
  3. ...
  4. "sideEffects": [
  5. "./src/side-effect.js" // 若为数组,则表示除了数组中的模块,其余都没有副作用,可以放心删除
  6. ],
  7. "devDependencies": {
  8. "webpack": "^5.38.1",
  9. "webpack-cli": "^4.7.0"
  10. }
  11. }

则打包的文件中会包含对应代码

./dist/bundle.js

  1. (()=>{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。

  1. new webpack.optimize.ModuleConcatenationPlugin()