webpack Tree Shaking 之 sideEffects

image

例子 🌰

下面以一个业务项目为例, 我们添加了若干文件来验证测试

  • 入口文件: src/views/act-choose-goods/index.ts
  • 期望结果: 经过 webpack Tree Shaking 后没有使用到的 aaa.tsddd.ts 都会被删除
1
2
3
4
// src/views/act-choose-goods/index.ts

import { ccccccccc } from "./bbb"
console.log(ccccccccc)
1
2
3
4
// src/views/act-choose-goods/bbb.ts

export { aaaaaaaaa } from "./aaa"
export { ccccccccc } from "./ccc"
1
2
3
4
// src/views/act-choose-goods/aaa.ts

import { ddddddddd } from "./ddd"
export const aaaaaaaaa = 'aaaaaaaaa'
1
2
3
// src/views/act-choose-goods/ccc.ts

export const ccccccccc = 'ccccccccc'
1
2
3
// src/views/act-choose-goods/ddd.ts

export const ddddddddd = 'ddddddddd'
  • 实际结果: aaa.ts 还存在,只有 ddd.ts 被删除了,那么 webpack 为什么没有删除 aaa.ts ?

image

原因是 webpack Tree Shaking 的实现原理 中提到的, 由于ddddddddd is declared but its value is never read.被 ts-loader 给删除了,我们篡改下 ts-loader 的代码,使得它保留 import { ddddddddd } from "./ddd" 这行代码
image

现在我们发现打包后 ddd.ts 也被保留了下来…
image

原因分析

为什么 webpack 没有删除未使用到的 aaa.tsddd.ts 模块?

原因是 webpack 无法确认 aaa.tsddd.ts 是否有副作用。比如我们常在代码中这样去 import 一个 polyfill 来兼容低版本浏览器, 在这种情况下我们虽然没有使用 react-app-polyfill 的导出, 但是不能删除 import 'react-app-polyfill/ie11' 这行代码

1
2
3
import 'react-app-polyfill/ie11';

// ...

因为react-app-polyfill/ie11直接修改了 window、Object 等全局对象, 这段代码有副作用, 即使没有用到其导出也应该被保留下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// react-app-polyfill/ie11

'use strict';

if (typeof Promise === 'undefined') {
// Rejection tracking prevents a common issue where React gets into an
// inconsistent state due to an error, but it gets swallowed by a Promise,
// and the user has no idea what causes React's erratic future behavior.
require('promise/lib/rejection-tracking').enable();
self.Promise = require('promise/lib/es6-extensions.js');
}

// Make sure we're in a Browser-like environment before importing polyfills
// This prevents `fetch()` from being imported in a Node test environment
if (typeof window !== 'undefined') {
// fetch() polyfill for making API calls.
require('whatwg-fetch');
}

// Object.assign() is commonly used with React.
// It will use the native implementation if it's present and isn't buggy.
Object.assign = require('object-assign');

// Support for...of (a commonly used syntax feature that requires Symbols)
require('core-js/features/symbol');
// Support iterable spread (...Set, ...Map)
require('core-js/features/array/from');

又比如当你没用 CSS Modules 时, 通常只需 import 'antd/dist/reset.css';, 此时也不会用到 *.css 模块的导出, 说明 *.css 模块也有副作用不能轻易被删除

对于这种情况, 我们可以在项目的 package.json 中可以通过 sideEffects 字段声明哪些文件是有副作用, 如下表示仅 *.css 模块有副作用

1
2
3
"sideEffects": [
"*.css"
]

当确认了 *.ts 没有副作用后, 再看一下结果发现 aaa.tsddd.ts 最终被成功删除了
image

实现原理

当我们没有在 package.json 中声明 sideEffects 字段时, 可以看到对于 aaa.ts 模块的 hasSideEffects 为 true, 即是有副作用的, 那么对于 *.ts 模块的 factoryMeta.sideEffectFree 的值都将为默认的 undefined
image
当我们声明 sideEffects 字段后, 那么某个模块的文件后缀会与 sideEffects 进行类似正则匹配, 对于 *.ts 模块没有被 *.css 表达式匹配上则 hasSideEffects 为 false, factoryMeta.sideEffectFree 被赋值为 true

即 sideEffects 字段决定 factoryMeta.sideEffectFree 的值, 而 factoryMeta.sideEffectFree 的值将决定该模块是否被 Tree Shaking

  • 对于 ccc.ts 模块的导出且有被实际使用到, ccc.ts 模块对应的是 HarmonyImportSpecifierDependency
  • 对于 aaa.tsddd.ts 模块的导出没有被使用到, 故被分配的是 HarmonyImportSideEffectDependency(从这里大概就可以猜出, 该类型模块如果没有副作用后续会被删除)

image

下面讲一下 HarmonyImportSideEffectDependency, 如上图 iteratorDependency 函数中当 ref 存在 && ref.module 也存在等条件成立时也会被添加到 blockInfoModules 中(可以认为没有添加到 blockInfoModules 中的模块是不会生成到打包后的代码中)。

那么最关键的因素就是 this._module.factoryMeta.sideEffectFree 的值, 如果值为 true, 那么 getDependencyReference 函数返回值为 null, ref 为 null 就结束了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack/lib/Compilation.js

class Compilation {
getDependencyReference(module, dependency) {
// TODO remove dep.getReference existence check in webpack 5
if (typeof dependency.getReference !== 'function') return null
const ref = dependency.getReference()
if (!ref) return null
return this.hooks.dependencyReference.call(ref, dependency, module)
}
}

// webpack/lib/dependencies/HarmonyImportSideEffectDependency.js

class HarmonyImportSideEffectDependency extends HarmonyImportDependency {
getReference() {
if (this._module && this._module.factoryMeta.sideEffectFree) return null;

return super.getReference();
}
}

factoryMeta.sideEffectFree 的值其实我们上面已经讨论过了

  • 当没有声明 sideEffects 字段时, factoryMeta.sideEffectFree 的值为 undefined, 继续调用 super.getReference() 将会返回一个有值的 ref
  • 当声明 sideEffects 字段后, factoryMeta.sideEffectFree 的值为 true, ref 即为 null, 自此被引用了但未实际用到其导出的 aaa.tsddd.tsbbb.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

image

  • 对于一个脚手架: 可以给同一类文件统一加上 sideEffects, 详细见 Rule.sideEffects
    image