寫於 2018.08.30webpack
webpack 2.0 開始引入 tree shaking 技術。在介紹技術以前,先介紹幾個相關概念:git
AST 對 JS 代碼進行語法分析後得出的語法樹 (Abstract Syntax Tree)。AST語法樹能夠把一段 JS 代碼的每個語句都轉化爲樹中的一個節點。github
DCE Dead Code Elimination,在保持代碼運行結果不變的前提下,去除無用的代碼。這樣的好處是:web
而所謂 Dead Code 主要包括:json
tree shaking 是 DCE 的一種方式,它能夠在打包時忽略沒有用到的代碼。數組
tree shaking 是 rollup 做者首先提出的。這裏有一個比喻:bash
若是把代碼打包比做製做蛋糕。傳統的方式是把雞蛋(帶殼)所有丟進去攪拌,而後放入烤箱,最後把(沒有用的)蛋殼所有挑選並剔除出去。而 treeshaking 則是一開始就把有用的蛋白蛋黃放入攪拌,最後直接做出蛋糕。babel
所以,相比於排除不使用的代碼,tree shaking 實際上是找出使用的代碼。閉包
基於ES6
的靜態引用,tree shaking 經過掃描全部 ES6 的export
,找出被import
的內容並添加到最終代碼中。 webpack 的實現是把全部import
標記爲有使用/無使用兩種,在後續壓縮時進行區別處理。由於就如比喻所說,在放入烤箱(壓縮混淆)前先剔除蛋殼(無使用的import
),只放入有用的蛋白蛋黃(有使用的import
)架構
首先源碼必須遵循 ES6 的模塊規範 (import
&export
),若是是 CommonJS 規範 (require
) 則沒法使用。
根據 Webpack 官網的提示,webpack2 支持 tree-shaking,須要修改配置文件,指定 babel 處理 js 文件時不要將 ES6 模塊轉成 CommonJS 模塊,具體作法就是:
在 .babelrc 設置 babel-preset-es2015 的 modules 爲 fasle,表示不對 ES6 模塊進行處理。
// .babelrc
{
"presets": [
["es2015", {"modules": false}]
]
}
複製代碼
通過測試,webpack 3 和 4 不增長這個 .babelrc 文件也能夠正常 tree shaking
webpack 負責對代碼進行標記,把import
&export
標記爲 3 類:
import
標記爲/* harmony import */
export
標記爲/* harmony export ([type]) */
,其中[type]
和 webpack 內部有關,多是binding
, immutable
等等。export
標記爲/* unused harmony export [FuncName] */
,其中 [FuncName]
爲export
的方法名稱以後在 Uglifyjs (或者其餘相似的工具) 步驟進行代碼精簡,把沒用的都刪除。
全部實例代碼均在 demo/webpack 目錄
// index.js
import {hello, bye} from './util'
let result1 = hello()
console.log(result1)
複製代碼
// util.js
export function hello () {
return 'hello'
}
export function bye () {
return 'bye'
}
複製代碼
編譯後的 bundle.js 以下:
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util__ = __webpack_require__(1);
let result1 = Object(__WEBPACK_IMPORTED_MODULE_0__util__["a" /* hello */])()
console.log(result1)
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = hello;
/* unused harmony export bye */
function hello () {
return 'hello'
}
function bye () {
return 'bye'
}
複製代碼
注:省略了bundle.js
上邊 webpack 自定義的模塊加載代碼,那些都是固定的。
對於沒有使用的bye
方法,webpack 標記爲unused harmony export bye
,可是代碼依舊保留。而hello
就是正常的harmony export (immutable)
。
以後使用UglifyJSPlugin
就能夠進行第二步,把bye
完全清除,結果以下:
只有hello
的定義和調用。
// index.js
import Util from './util'
let util = new Util()
let result1 = util.hello()
console.log(result1)
複製代碼
// util.js
export default class Util {
hello () {
return 'hello'
}
bye () {
return 'bye'
}
}
複製代碼
編譯後的 bundle.js 以下:
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util__ = __webpack_require__(1);
let util = new __WEBPACK_IMPORTED_MODULE_0__util__["a" /* default */]()
let result1 = util.hello()
console.log(result1)
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
class Util {
hello () {
return 'hello'
}
bye () {
return 'bye'
}
}
/* harmony export (immutable) */ __webpack_exports__["a"] = Util;
複製代碼
注意到 webpack 是對Util
類總體進行標記的(標記爲被使用),而不是分別針對兩個方法。也所以,最終打包的代碼依然會包含bye
方法。這代表 webpack tree shaking 只處理頂層內容,例如類和對象內部都不會再被分別處理。
這主要也是因爲 JS 的動態語言特性所致。若是把bye()
刪除,考慮以下代碼:
// index.js
import Util from './util'
let util = new Util()
let result1 = util[Math.random() > 0.5 ? 'hello', 'bye']()
console.log(result1)
複製代碼
編譯器並不能識別一個方法名字到底是以直接調用的形式出現 (util.hello()
) 仍是以字符串的形式 (util['hello']()
) 或者其餘更加離奇的方式。所以誤刪方法只會致使運行出錯,得不償失。
反作用的意思某個方法或者文件執行了以後,還會對全局其餘內容產生影響的代碼。例如 polyfill 在各種prototype
加入方法,就是反作用的典型。(也能夠看出,程序和吃藥不一樣,反作用不全是貶義的)
反作用總共有兩種形態,是精簡代碼不得不考慮的問題。咱們平時在重構代碼時,也應當以相相似的思惟去進行,不然總有踩坑的一天。
// index.js
import Util from './util'
console.log('Util unused')
複製代碼
// util.js
console.log('This is Util class')
export default class Util {
hello () {
return 'hello'
}
bye () {
return 'bye'
}
}
Array.prototype.hello = () => 'hello'
複製代碼
如上代碼通過webpack + uglify
的處理後,會變成這樣:
雖然Util
類被引入以後沒有進行任何使用,可是不能當作沒引用過而直接刪除。在混合後的代碼中,能夠看到Util
類的本體 (export
的內容) 已經沒有了,可是先後的 console.log
和對Array.prototype
的擴展依然保留。這就是編譯器爲了確保代碼執行效果不變而作的妥協,由於它不知道這兩句代碼究竟是幹嗎的,因此他默認認定全部代碼 均有 反作用。
// index.js
import {hello, bye} from './util'
let result1 = hello()
let result2 = bye()
console.log(result1)
複製代碼
// util.js
export function hello () {
return 'hello'
}
export function bye () {
return 'bye'
}
複製代碼
咱們引入並調用了bye()
,可是卻沒有使用它的返回值result2
,這種代碼能夠刪嗎?(捫心自問,若是是你人肉重構代碼,直接刪掉這行代碼的可能性有沒有超過 90% ?)
webpack 並無刪除這行代碼,至少沒有刪除所有。它確實刪除了result2
,但保留了 bye()
的調用(壓縮的代碼表現爲Object(r.a)()
)以及bye()
的定義。
這一樣是由於編譯器不清楚bye()
裏面究竟作了什麼。若是它包含了如Array.prototye
的擴展,那刪掉就又出問題了。
咱們很感謝 webpack 如此嚴謹,但若是某個方法就是沒有反作用的,咱們該怎麼告訴 webpack 讓他放心大膽的刪除呢?
有 3 個方法,適用於不一樣的狀況。
// index.js
import {hello, bye} from './util'
let result1 = hello()
let a = 1
let b = 2
let result2 = Math.floor(a / b)
console.log(result1)
複製代碼
util.js 和以前相同,再也不重複。有差異的是 webpack.config.js,須要增長參數pure_funcs
,告訴webpack Math.floor
是沒有反作用的,你能夠放心刪除:
plugins: [
new UglifyJSPlugin({
uglifyOptions: {
compress: {
pure_funcs: ['Math.floor']
}
}
})
],
複製代碼
在添加了pure_funcs
配置後,原來保留的Math.floor(.5)
被刪除了,達到了咱們的預期效果。
但這個方法有一個很大的侷限性,在於若是咱們把 webpack 和 uglify 合併使用,通過 webpack 的代碼的方法名已經被重命名了,那麼在這裏配置原始的方法名也就失去了意義。而例如Math.floor
這類全局方法不會重命名,纔會生效。所以適用性不算太強。
webpack 4 在 package.json 新增了一個配置項叫作sideEffects
, 值爲false
表示整個包都沒有反作用;或者是一個數組列出有反作用的模塊。詳細的例子能夠查看 webpack 官方提供的例子。
從結果來看,若是sideEffects
值爲false
,當前包export
了 5 個方法,而咱們使用了 2 個,剩下 3 個也不會被打包,是符合預期的。但這要求包做者的自覺添加,所以在當前 webpack 4 推出不久的狀況下,侷限性也不算小。
webpack 3 開始加入了webpack.optimize.ModuleConcatenateModulePlugin()
,到了 webpack 4 直接做爲 `mode = 'production' 的默認配置。這是對 webpack bundle 的一個優化,把原本「每一個模塊包裹在一個閉包裏」的狀況,優化成「全部模塊都包裹在同一個閉包裏」的狀況。自己對於代碼縮小體積有很大的提高,這裏也能側面解決反作用的問題。
依然選取這樣 2 個文件做爲例子:
// index.js
import {hello, bye} from './util'
let result1 = hello()
let result2 = bye()
console.log(result1)
複製代碼
// util.js
export function hello () {
return 'hello'
}
export function bye () {
return 'bye'
}
複製代碼
在開啓了 concatenateModule 功能後,打包出來的代碼以下:
首先,bye()
方法的調用和本體都被消除了。
其次,hello()
方法的調用和定義被合成到了一塊兒,變成直接console.log('hello')
第三就是這個功能原有的目的:代碼量減小了。
這個功能的本意是把全部模塊最終輸出到同一個方法內部,從而把調用和定義合併到一塊兒。這樣像bye()
這樣沒有反作用的方法就能夠在合併以後被輕易識別出來,並加以刪除。有關這個功能更加詳細的介紹能夠看這篇文章