例子 🌰
下面以一个业务项目为例, 我们添加了若干文件来验证测试
- 入口文件:
src/views/act-choose-goods/index.ts
- 期望结果: 经过 webpack Tree Shaking 后没有使用到的
aaa.ts
与ddd.ts
都会被删除
1 | // src/views/act-choose-goods/index.ts |
1 | // src/views/act-choose-goods/bbb.ts |
1 | // src/views/act-choose-goods/aaa.ts |
1 | // src/views/act-choose-goods/ccc.ts |
1 | // src/views/act-choose-goods/ddd.ts |
- 实际结果:
aaa.ts
还存在,只有ddd.ts
被删除了,那么 webpack 为什么没有删除aaa.ts
?
原因是 webpack Tree Shaking 的实现原理 中提到的, 由于ddddddddd is declared but its value is never read.
被 ts-loader 给删除了,我们篡改下 ts-loader 的代码,使得它保留 import { ddddddddd } from "./ddd"
这行代码
现在我们发现打包后 ddd.ts
也被保留了下来…
原因分析
为什么 webpack 没有删除未使用到的 aaa.ts
与 ddd.ts
模块?
原因是 webpack 无法确认 aaa.ts
与 ddd.ts
是否有副作用。比如我们常在代码中这样去 import 一个 polyfill 来兼容低版本浏览器, 在这种情况下我们虽然没有使用 react-app-polyfill 的导出, 但是不能删除 import 'react-app-polyfill/ie11'
这行代码
1 | import 'react-app-polyfill/ie11'; |
因为react-app-polyfill/ie11
直接修改了 window、Object 等全局对象, 这段代码有副作用, 即使没有用到其导出也应该被保留下来
1 | // react-app-polyfill/ie11 |
又比如当你没用 CSS Modules 时, 通常只需 import 'antd/dist/reset.css';
, 此时也不会用到 *.css 模块的导出, 说明 *.css 模块也有副作用不能轻易被删除
对于这种情况, 我们可以在项目的 package.json
中可以通过 sideEffects 字段声明哪些文件是有副作用, 如下表示仅 *.css 模块有副作用
1 | "sideEffects": [ |
当确认了 *.ts 没有副作用后, 再看一下结果发现 aaa.ts
与 ddd.ts
最终被成功删除了
实现原理
当我们没有在 package.json
中声明 sideEffects 字段时, 可以看到对于 aaa.ts 模块的 hasSideEffects 为 true, 即是有副作用的, 那么对于 *.ts 模块的 factoryMeta.sideEffectFree
的值都将为默认的 undefined
当我们声明 sideEffects 字段后, 那么某个模块的文件后缀会与 sideEffects 进行类似正则匹配, 对于 *.ts 模块没有被 *.css 表达式匹配上则 hasSideEffects 为 false, factoryMeta.sideEffectFree
被赋值为 true
即 sideEffects 字段决定 factoryMeta.sideEffectFree
的值, 而 factoryMeta.sideEffectFree
的值将决定该模块是否被 Tree Shaking
- 对于
ccc.ts
模块的导出且有被实际使用到,ccc.ts
模块对应的是 HarmonyImportSpecifierDependency- 对于
aaa.ts
、ddd.ts
模块的导出没有被使用到, 故被分配的是 HarmonyImportSideEffectDependency(从这里大概就可以猜出, 该类型模块如果没有副作用后续会被删除)
下面讲一下 HarmonyImportSideEffectDependency, 如上图 iteratorDependency 函数中当 ref 存在 && ref.module 也存在等条件成立时也会被添加到 blockInfoModules 中(可以认为没有添加到 blockInfoModules 中的模块是不会生成到打包后的代码中)。
那么最关键的因素就是 this._module.factoryMeta.sideEffectFree
的值, 如果值为 true, 那么 getDependencyReference 函数返回值为 null, ref 为 null 就结束了
1 | // webpack/lib/Compilation.js |
factoryMeta.sideEffectFree
的值其实我们上面已经讨论过了
- 当没有声明 sideEffects 字段时,
factoryMeta.sideEffectFree
的值为 undefined, 继续调用 super.getReference() 将会返回一个有值的 ref - 当声明 sideEffects 字段后,
factoryMeta.sideEffectFree
的值为 true, ref 即为 null, 自此被引用了但未实际用到其导出的aaa.ts
、ddd.ts
、bbb.ts
等模块都不会添加到 blockInfoModules 中,即不会出现在打包后的文件中
关于 HarmonyImportSpecifierDependency 在 webpack Tree Shaking 的实现原理 中说过, 比如模块
index.ts
引用了ccc.ts
的导出, 那么 refModule(ccc.ts
模块)最后会被添加到 blockInfoModules 中(此时 currentModule 为index.ts
, d 为 HarmonyImportSpecifierDependency, refModule 为ccc.ts
)
小结
- 对于一个业务项目: 可以在 package.json 中声明好 sideEffects 字段来辅助 webpack 进行 Tree Shaking
1
2
3
4"sideEffects": [
"*.css",
"*.scss"
] - 对于一个 npm 包: 同理, 详细见 ant-design/package.json
- 对于一个脚手架: 可以给同一类文件统一加上 sideEffects, 详细见 Rule.sideEffects