Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack Tree Shaking 之 sideEffects #52

Open
xiaoxiaojx opened this issue Jan 16, 2023 · 0 comments
Open

webpack Tree Shaking 之 sideEffects #52

xiaoxiaojx opened this issue Jan 16, 2023 · 0 comments
Labels
webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a

Comments

@xiaoxiaojx
Copy link
Owner

xiaoxiaojx commented Jan 16, 2023

image

例子 🌰

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

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

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

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

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

export const ccccccccc = 'ccccccccc'
// 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' 这行代码

import 'react-app-polyfill/ie11';

// ...

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

// 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 模块有副作用

"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 就结束了

// 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
"sideEffects": [
    "*.css",
    "*.scss"
 ]

image

  • 对于一个脚手架: 可以给同一类文件统一加上 sideEffects, 详细见 Rule.sideEffects
    image
@xiaoxiaojx xiaoxiaojx added the webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a label Jan 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a
Projects
None yet
Development

No branches or pull requests

1 participant