lmk123 / blog

个人技术博客,博文写在 Issues 里。
https://github.com/lmk123/blog/issues
623 stars 35 forks source link

Webpack 的 `sideEffects` #130

Open lmk123 opened 4 months ago

lmk123 commented 4 months ago

以前,我对 sideEffects: false 的理解仅停留在以下两个方面:

作为模块开发者,我对于如何正确的使用 sideEffects 才能让代码正常运行一直都很模糊,今天我觉得我终于弄懂了这个问题,于是记录一下。

什么代码算是有副作用(side effect)的?

Webpack 对副作用的定义是:(来源

A "side effect" is defined as code that performs a special behavior when imported, other than exposing one or more exports. An example of this are polyfills, which affect the global scope and usually do not provide an export. 所谓 "副作用",是指代码在导入时,除了公开一个或多个导出外,还执行其他特殊行为。polyfills 就是一个例子,它会影响全局范围,通常不提供导出。

我对于这个问题的理解是:任何会对模块以外产生影响的代码。

比如下面这个模块 a.js

// a.js

console.log('a.js')

export function method() {
  console.log('运行模块导出的方法')
}

假设另一个模块 b.js 导入了 a.js 的 method 方法,但没有运行:

// b.js

import { method } from './a.js'

console.log('b.js')

// 虽然导入了 method 但没有运行

当你使用 Webpack 打包 b.js 时,它会把上面那条导入优化成 import './a',而 a.js 里的 method() 代码会被去掉,但保留了 console.log('a.js'),以确保控制台是能打印出来这条消息的。

现在来解释一下为什么 console.log('a.js') 是有副作用的。

它在控制台打印的日志很有可能是你需要的——也许你的需求就是要确保能在控制台看到这条日志。你也有可能不需要,但 Webpack 并不知道你是否需要,所以保险起见它会保留这行代码,那么这条代码阻止了 Webpack 删除整个 a.js。

所以,console.log('a.js') 是有副作用的,因为你可能在 a.js 模块以外需要它。

这种优化方法似乎正合我意——那 sideEffects: false 是做什么的?

这里面有一个关键的部分:你可能并不需要你的副作用代码

在上面的例子当中,console.log('a.js') 语句很有可能在生产环境并不需要,但由于 Webpack 不确定你是否需要,所以它保留了。

如果你显式的声明了 sideEffects: false,其实就是在告诉 Webpack:我的所有代码都没有副作用,可以安全删掉。

声明之后,Webpack 就会把 b.js 里的整条 import { method } from './a.js' 删掉,这么一对比,就减少了捆绑包的体积。

判断一个模块是否有副作用的关键

我的理解如下:如果这个模块的导出项都没有被 import 过,那么模块里的其它代码是否可以删除并且不会对其它代码运行产生影响?

如果可以删除,那么说明这个模块是没有副作用的。

如果不可以,那么就说明这个模块是有副作用的。

这也就解释了我之前的一个疑惑:Vue.js 也标记了 sideEffects: false

要知道,Vue.js 代码是充满我认为的副作用的,比如修改原型链。

以下举例:

// my.js

const _push = Array.prototype.push
Array.prototype.push = function () {
  console.log('我修改了 prototype')
  return _push.apply(this, arguments)
}

export function call(arr) {
  arr.push(1)
}

提问:上面的模块可以标记为 sideEffects: false 吗?

只需要想一下,这段对原型链的修改代码如果被删除了,是否会对其他代码产生影响,就能得出答案。

是否可以被安全删除的 sideEffects 举例

所以,产生副作用的代码是否可以被安全删除是问题的关键,现在来看几个例子。

导入 CSS 文件

// app.js

import './style.css'

console.log('app.js')

在上面的代码中,import './style.css' 产生了副作用,因为它会让 Webpack 生成一个单独的 CSS 文件并一般会被自动插入到 html 文件当中。如果它被删除了,你的 Web App 就没有样式了,这显然对模块之外的代码产生了影响。

但是,如果你声明了 sideEffects: false,Webpack 就会把 import './style.css' 删掉——因为你没有用到 style.css 这个模块里的任何导出项,并且你告诉了 Webpack “我的所有代码里的副作用都不需要,可以安全删掉。”,于是 Webpack 就把它删掉了。

换句话说,import './style.css' 是你的必要副作用,所以你不能声明 sideEffects: false,或者使用以下方式告诉 Webpack,style.css 以外的其它模块的副作用才能删除:

"sideEffects": ["style.css"]

但是,如果是下面这样:

import style from './style.css'

const styleEle = document.createElement('style')
styleEle.textContent = style
document.head.appendChild(styleEle)

现在,样式被当成字符串引入到了模块内部,Webpack 就不会把它单独抽离成一个 CSS 文件了,继而就不会影响到 Web App 的样式,所以 import style form './style.css' 本身是没有副作用的。

但是,在 document 中插入 style 元素的代码仍然是有副作用的,要想避免 app.js 产生副作用,就需要把产生副作用的代码用函数包裹起来并 export 出去:

// 没有副作用的 app.js

import style from './style.css' // 没有副作用

const styleEle = document.createElement('style') // 没有副作用
styleEle.textContent = style // 没有副作用

export function appendStyle() {
  document.head.appendChild(styleEle) // 有副作用,因为影响了 document 的样式
}

这样一来,只有运行了 appendStyle() 的模块才会产生副作用,而 app.js 本身是没有副作用的。

参考资料

Ref: #120