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 中的 sideEffects 到底该怎么用? #41

Open
kuitos opened this issue Jul 18, 2018 · 2 comments
Open

Webpack 中的 sideEffects 到底该怎么用? #41

kuitos opened this issue Jul 18, 2018 · 2 comments

Comments

@kuitos
Copy link
Owner

kuitos commented Jul 18, 2018

webpack v4 开始新增了一个 sideEffects 特性,通过给 package.json 加入 sideEffects: false 声明该包模块是否包含 sideEffects(副作用),从而可以为 tree-shaking 提供更大的优化空间。

先看张图感受一下:

注:v4 beta 版时叫 pure module, 后来改成了 sideEffects

基于我们对 fp 中的 side effect 的理解,我们可以认为,只要我们确定当前包里的模块不包含副作用,然后将发布到 npm 里的包标注为 sideEffects: false ,我们就能为使用方提供更好的打包体验。原理是 webpack 能将标记为 side-effects-free 的包由 import {a} from xx 转换为 import {a} from 'xx/a',从而自动修剪掉不必要的 import,作用同 babel-plugin-import

于是很愉快的我给我的几个库都加上了这个配置(确定都不含副作用)。

直到我几个月前看到 @sean Larkin 给 vue 提交了这样一个 pr:chore(package.json): Add sideEffects: false field in package.json, 当时我就有点疑惑,依我对 vue 的了解,代码里的副作用挺多啊,比如很多函数都有对 Vue.prototype 的引用甚至修改,应该不能设置 sideEffects: false 才对啊。然而事实是我被打脸了,因为尤大很快的合并了这个 pr。。这直接导致我不敢给 mobx 加上这个配置,因为已经完全不明白 webpack 的这个 sideEffects 指的是什么了。。

直到前两天有人给 mobx-utils 提了 issue 说可以加上这个配置帮助 tree shaking,疑惑中我想起了 vue 的那个 pr 又翻出来看了一遍,发现在 pr 下已经有人跟我提了一样的疑问:

Hy Sean!

Could you please specify what you mean by "vue's original source files"?

I looked at the index.js file in the src/core folder and to my knowledge there are plenty sideeffects that would be prune away by tree shaking. (e.g Object.defineProperty)

I hope you can help me understand how this works.

Sean 原来的 pr 里是这样写的:

This PR adds the "sideEffects": false property in vue's package.json file. This allow's webpack (for those who want to opt-in to requiring vue's original source files (instead of the flattened esm bundles) and want to remove flow type through a babel-transform, then this will allow webpack to aggressively ignore and treeshake unused exports throughout the module system.

Sean 的意思是当你按需引入 vue 的源码文件而不是打包的 bundle 时,webpack 能帮助你做更好的 tree shaking。比如你这样引用 vue 中的模块:import Vue from 'vue/src/core'

然后 Sean 就说此副作用非彼副作用(fp 中的),然后给了一个他在 stackoverflow 上的回答来解释 sideEffects,中心思想是:

whenever a module reexports all exports (regardless if used or unused) need to be evaluated and executed in the case that one of those exports created a side-effect with another.

每当一个模块重导出了所有导出(无论是否会被用) 需要被计算和执行时,其中一个导出就对其他的导出产生了副作用。

老实讲还是没懂。。有兴趣的看原答案:what-does-webpack-4-expect-from-a-package-with-sideeffects-false

翻完 官方文档官方 example,只是了解到有了 sideEffects 后 bundle 的变化,依然无法解释 webpack sideEffects 跟 fp 中的 sideEffect 有什么区别,进而也无法解释为什么 vue 明明很多副作用依然能配置 sideEffects: false ?

毛主席教导我们:自力更生,丰衣足食。

Tree Shaking 与副作用

Tree Shaking 的背景就不介绍了想必很多人都了解,webpack 的 tree shaking 的作用是可以将未被使用的 exported member 标记为 unused 同时在将其 re-export 的模块中不再 export。说起来很拗口,看代码:

// a.js
export function a() {}
// b.js
export function b(){}
// package/index.js
import a from './a'
import b from './b'
export { a, b }
// app.js
import {a} from 'package'
console.log(a)

当我们已 app.js 为 entry 时,经过摇树后的代码会变成这样:

// a.js
export function a() {}
// b.js 不再导出 function b(){}
function b() {}
// package/index.js 不再导出 b 模块
import a from './a'
import b from './b'
export { a }
// app.js
import {a} from 'package'
console.log(a)

配合 webpack 的 scope hoisting 和 uglify 之后,b 模块的痕迹会被完全抹杀掉。

但是如果 b 模块中添加了一些副作用,比如一个简单的 log:

// b.js
export function b(v) { reutrn v }
console.log(b(1))

webpack 之后会发现 b 模块内容变成了:

// b.js
console.log(function (v){return v}(1))

虽然 b 模块的导出是被忽略了,但是副作用代码被保留下来了。由于目前 transformer 转换后可能引入的各种奇怪操作引发的副作用(参考:你的Tree-Shaking并没什么卵用),很多时候我们会发现就算有了 tree shaking 我们的 bundle size 还是没有明显的减小。而通常我们期望的是 b 模块既然不被使用了,其中所有的代码应该不被引入才对。

这个时候 sideEffects 的作用就显现出来了:如果我们引入的 包/模块 被标记为 sideEffects: false 了,那么不管它是否真的有副作用,只要它没有被引用到,整个 模块/包 都会被完整的移除。以 mobx-react-devtool 为例,我们通常这样去用:

import DevTools from 'mobx-react-devtools';

class MyApp extends React.Component {
  render() {
    return (
      <div>
        ...
        { process.env.NODE_ENV === 'production' ? null : <DevTools /> }
      </div>
    );
  }
}

这是一个很常见的按需导入场景,然而在没有 sideEffects: false 配置时,即便 NODE_ENV 设为 production ,打包后的代码里依然会包含 mobx-react-devtools 包,虽然我们没使用过其导出成员,但是 mobx-react-devtools 还是会被 import,因为里面“可能”会有副作用。但当我们加上 sideEffects false 之后,tree shaking 就能安全的把它从 bundle 里完整的移除掉了。

sideEffects 的使用场景

上面也说到,通常我们发布到 npm 上的包很难保证其是否包含副作用(可能是代码的锅可能是 transformer 的锅),但是我们基本能确保这个包是否会对包以外的对象产生影响,比如是否修改了 window 上的属性,是否复写了原生对象方法等。如果我们能保证这一点,其实我们就能知道整个包是否能设置 sideEffects: false了,至于是不是真的有副作用则并不重要,这对于 webpack 而言都是可以接受的。这也就能解释为什么能给 vue 这个本身充满副作用的包加上 sideEffects: false 了。

所以其实 webpack 里的 sideEffects: false 的意思并不是我这个模块真的没有副作用,而只是为了在摇树时告诉 webpack:我这个包在设计的时候就是期望没有副作用的,即使他打完包后是有副作用的,webpack 同学你摇树时放心的当成无副作用包摇就好啦!

也就是说,只要你的包不是用来做 polyfill 或 shim 之类的事情,就尽管放心的给他加上 sideEffects: false 吧!

@chaoren1641
Copy link

chaoren1641 commented Jul 18, 2018

看了下好像需要引用的包修改 package.json 才行,有没有能在 webpack.config.js 中指定包名的方式呢。

@kuitos
Copy link
Owner Author

kuitos commented Jul 19, 2018

@chaoren1641 可以在 loader 的 rule 里对指定 包/模块 声明 sideEffects
https://webpack.js.org/configuration/module/#rule-sideeffects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants