Webpack 中的sideEffects 到底該怎麼用?

Webpack 中的sideEffects 到底該怎麼用?

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

先看張圖感受一下:

https://twitter.com/jdalton/status/893109185264500737

注:v4 beta版時叫pure module ,後來改成了sideEffects

基於我們對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. (eg 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 上的回答來解釋webpack 裡的這個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' 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吧!

What do you think?

Written by marketer

深層屬性,輕鬆提取

前端佈道者克軍確認出席中國React大會,並發表主題演講