xiaoxiaojx / blog

Project for records problems solved in my work and study.
https://xiaoxiaojx.github.io/
MIT License
252 stars 6 forks source link

webpack Tree Shaking 之 sideEffects #52

Open xiaoxiaojx opened 1 year ago

xiaoxiaojx commented 1 year ago

image

例子 🌰

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

// 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'

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 的值其实我们上面已经讨论过了

关于 HarmonyImportSpecifierDependency 在 webpack Tree Shaking 的实现原理 中说过, 比如模块 index.ts 引用了 ccc.ts 的导出, 那么 refModule(ccc.ts 模块)最后会被添加到 blockInfoModules 中(此时 currentModule 为 index.ts, d 为 HarmonyImportSpecifierDependency, refModule 为 ccc.ts

小结

image