Webpack 中的 sideEffects 到底該怎麼用?

Kuitos發表於2018-07-19

原文連結

webpack v4 開始新增了一個 sideEffects 特性,通過給 package.json 加入 sideEffects: false 宣告該包模組是否包含 sideEffects(副作用),從而可以為 tree-shaking 提供更大的優化空間。

先看張圖感受一下:

Webpack 中的 sideEffects 到底該怎麼用?

注: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 吧!

相關文章