Description
一、作用
在webpack对模块进行打包时,将模块中未被使用的冗余代码剔除,仅打包有效代码,精简生成包的体积。
如何编写易于 Tree shaking 的代码
将每个工具函数单独 export
,不要集成为一个class
,tree shaking
的最小单元是一个对象,它不能识别一个对象中的哪些函数是需要的
二、如何使用
在 Webpack 中,启动Tree Shaking
功能必须同时满足三个条件:
- 使用 ESM 规范编写模块代码
- 配置
optimization.usedExports
为true
,启动标记功能 - 启动代码优化功能,可以通过如下方式实现:
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
三、实现原理
1. ESModule
我们通过对比ES Module
与CommonJS
的区别来理解ES Module
的模块机制,它们的区别主要体现在模块的输出和执行上,
ES Module
输出的是值的引用,而CommonJS
输出的是值的拷贝ES Module
是编译时执行,而CommonJS
模块是在运行时加载
所以ES Module
最大的特点就是静态化,在编译时就能确定模块的依赖关系,以及输入和输出的值,这意味着什么?意味着模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,正是基于这个基础,才使得 Tree-Shaking 成为可能,
2. webpack对模块打标记 、删除死代码
借助静态模块分析,Tree-Shaking
实现的大体思路:借助 ES6 模块语法的静态结构,通过编译阶段的静态分析,找到没有引入的模块并打上标记,然后在压缩阶段利用像uglify-js
这样的压缩工具删除这些没有用到的代码。
涉及知识:
- 压缩工具的作用:混淆,压缩,最小化,删除不可达代码等;
treeShaking
依赖于对模块导出和被导入的分析:optimization.providedExports
:确定每个模块的导出
,默认所有模式都开启optimization.usedExports
:确定每个模块下被使用的导出
。生产模式下默认开启,其他模式下不开启。
- webpack对代码进行标记,把
import
和export
标记为3类:- 所有
import
标记为/* harmony import */
被使用过的export
标记为/harmony export([type])/
没有被使用的export
标记为/* unused harmony export [FuncName] */
,其中[FuncName]为export的方法名
- 所有
四、开发模式和生产模式的默认配置存在差异,其打包方式也存在差异,这里分开讨论:
后面的说明围绕下例展开:
//my-module.js
//my-module.js
export const name = 123;
export const age = 9999;
//index.js
import {name, age} from './test.js';
console.log(name);
development模式
[optimization.usedExports:false]
1)webpack打包(uglifyWebpackPlugin处理前)
全部export
被标记为/* harmony export (binding) */
;
// my-module.js
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "name", function() { return name; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "age", function() { return age; });
var name = 123;
var age = 9999;
2)经过UglifyJSPlugin压缩后不会删除未被使用的导出;
// my-module.js
/* harmony export (binding) */ i.d(e, "name", function() {return t;}),
/* harmony export (binding) */ i.d(e, "age", function() {return o;});
var t = 123, o = 9999;
结论: 当usedExports:false
时,无法对未使用的接口做处理。
[optimization.usedExports: true]
1)webpack打包(uglifyWebpackPlugin处理前)
未被使用的export
会被标记为/* unused harmony export name */
,不会使用__webpack_require__.d
进行exports
绑定;
// my-module.js
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return name; });
/* unused harmony export age */
var name = 123;
var age = 9999;
可以看到age
被标记为unused
,同时没有使用__webpack_require__.d
链接exports
。
__webpack_require__.d
的作用如下:
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
2)经过UglifyJSPlugin
压缩后,未使用的接口代码会被删除(如果被别的模块import
导入但未被使用,同样会被剔除)。原理显而易见,age
未被__webpack_require__.d
引用。
// my-module.js
/* harmony export (binding) */ i.d(e, "a", function() {return t;});
/* unused harmony export age */
var t = 123;
不建议在开发环境使用压缩插件。
production模式
1)webpack打包(uglifyWebpackPlugin处理前)
由于生产环境内置了`ModuleConcatenationPlugin`插件,实现"预编译",让webpack根据模块间的关系依赖图中,将所有的模块连接成一个模块,称为"作用域提升"。对于代码缩小体积有很大的提升,也能侧面解决副作用的问题;每个模块会被标记`//CONCATENATED MODULE`。
//被打包到一个作用域内
(function(module, __webpack_exports__, __webpack_require__) {
//...
//CONCATENATED MODULE: ./src/my-module.js
var test_name = 123;
var age = 9999;
//CONCATENATED MODULE: ./src/index.js
console.log(test_name);
}
2)开启uglifyWebpackPlugin:
compress: true;
函数的调用会被用函数体替换,使用变量处用其对应值代替,将未使用的变量删除。压缩替换后如下:
function(e, n, o) {
//...
// CONCATENATED MODULE: ./src/index.js
console.log(123);
}
可以看到导入的age接口未被使用因此被删除,同时优化了多余的中间变量,代码得到精简。
五、sideEffects
在导入时会执行特殊行为的代码,而不是仅仅暴露一个export
或多个 export
。比如console.log()
、polyfills
、import a CSS file
等。由于编译器并不知道其是否会影响运行效果,故而不做处理。
在package.json中设置如何处理副作用:
// package.json
//false:表示该模块无副作用代码,若该模块的所有export没有被使用时,可直接删除该模块
//true:表示该模块有副作用代码,该模块要保留副作用代码
"sideEffects": [Boolean],
或者
// package.json
//[file1,file2]:指定有副作用的文件,在webpack作用域提升时就不会引入
"sideEffects": ['*.css', 'src/tool.js'],
情景1:
import { name } from './module.js';
//name没有使用
import './module.js'
name
没有被使用,module.js
中没有export
被使用,且若module.js
中包含副作用代码
:
sideEffects
为false
,则副作用也被删除。即module
整个模块都不会被打包;sideEffects
为true
或副作用列表中包含module.js
,则会仅保留其副作用代码。
情景2:
import { name } from './module.js';
console.log(name)
module.js
中name
接口被使用,未被使用的其余export
都会被删除;无论sidesEffects
设置什么值,module.js
中的副作用代码始终会被保留。