聊一聊面試中經常被問到的Tree Shaking
天下武功,唯快不破!最新版的 antd 以及 vue 都對 Tree Shaking 提供了支持。我們內部的組件在支持這部分功能時,也專門梳理了相關的特性。這是四月份寫的文章了,長時間不用就會忘,復習一下!
JS 文件絕大多數需要通過網絡進行加載,然后執(zhí)行。DCE(dead code elimination)可以使得加載文件的大小更小,整體執(zhí)行時間更短。tree shaking?就是通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊語法的?靜態(tài)結構?特性,例如?import?和?export。
原理
ESM
import 只能作為模塊頂層的語句出現 import 的模塊名只能是字符串常量 import binding 是 immutable 的
這就是區(qū)別于CMJ,ESM 獨有的靜態(tài)分析特性。等等,那什么是靜態(tài)分析呢,就是不執(zhí)行代碼。CMJ 中的 require,只有執(zhí)行以后才知道引用的是什么模塊。
保證了依賴關系是確定的,和運行時的狀態(tài)無關,可以進行可靠的靜態(tài)分析。靜態(tài)分析會在繪制依賴圖時做DCE,減少打包體積。
ESM 也支持動態(tài)引入,類似于下面這種引入方式是不支持Tree Shacking的。
if?(false)?{
??import('./a.js').then(()?=>?{//?...})
}?else?{
??//?...
}
//?antd.js
var?emptyObject?=?{};
if?(true)?{
??Object.freeze(emptyObject);
}
module.exports?=?emptyObject;
Dead Code
Dead Code 通常是指:
代碼不會被執(zhí)行 代碼執(zhí)行的結果不會被用到 代碼只會影響死變量(只寫不讀)
//?導入并賦值給?JavaScript?對象,但在接下來的代碼里沒有用到
//?這就會被當做“死”代碼,會被?tree-shaking
import?Stuff?from?'./stuff';
doSomething();
//?導入但沒有賦值給?JavaScript?對象,也沒有在代碼里用到
//?這會被當做“死”代碼,會被?tree-shaking
import?'./stuff';
doSomething();
//?全部導入?(不支持?tree-shaking)
import?_?from?'lodash';
//?具名導入(支持?tree-shaking)
import?{?debounce?}?from?'lodash';
//?直接導入具體的模塊?(支持?tree-shaking)
import?debounce?from?'lodash/lib/debounce';
//?導入并賦值給?JavaScript?對象,然后在下面的代碼中被用到
//?這會被看作“活”代碼,不會做?tree-shaking
import?Stuff?from?'./stuff';
doSomething(Stuff);
//?導入整個庫,但是沒有賦值給?JavaScript?對象,也沒有在代碼里用到
//?非常奇怪,這竟然被當做“活”代碼,因為 Webpack 對庫的導入和本地代碼導入的處理方式不同。
import?'my-lib';
export?{?default?as?Title?}?from?'./Title';
export?{?default?as?Options?}?from?'./Options';
export?{?default?as?AddonArea?}?from?'./AddonArea';
export?{?default?as?Answer?}?from?'./AddonArea/Answer';
export?{?default?as?Analysis?}?from?'./AddonArea/Analysis';
export?{?default?as?OriginalText?}?from?'./AddonArea/OriginalText';
export?{?default?as?Labels?}?from?'./AddonArea/Labels';
這樣的文件結構是無法進行 tree-shaking 的, 因為沒有 import?!
自執(zhí)行的模塊 import
自執(zhí)行模塊我們通常會使用?import 'xxx'?來進行模塊引用,而不進行顯式的調用。因此模塊本身就有副作用。
import?'utils/refresh'
對于這種模塊可以這樣處理:
在 sideEffects 中通過數組聲明,使其在 Tree Shaking 的范圍之外 模塊改造,暴露成員支持顯式調用
unused harmony export
如果該模塊被標識為 unused harmony export,則說明沒有外部引用使用到該成員,webpack 認為是可以安全去除的。
harmony export
部分被標識為 harmony export 的模塊也會被去除。這個是跟 UglifyJS 的機制有關系。
沒有提供導出成員的模塊
//?./src/modules/edu-discount/seckill/index.ts
import?*?as?SeckillTypes?from?'./types';
export?{?SeckillTypes?};
對于只有暴露的成員,但是沒有被引用的成員,這種模塊會被直接刪除。
[x] exports provided [ ] exports used
配置
babel的配置文件
{
??"presets":?[
????["env",?{
??????"modules":?false??//?配置了這個,babel就不會像默認那樣轉變成 require 形式。
????}],
????"stage-2",
????"react"
??]
}
為 webpack 進行 tree-shaking 創(chuàng)造了條件。
??不能引用類似?@babel/plugin-transform-modules-commonjs會把模塊編譯成 commonjs 的插件;
webpack 的配置文件
webpack 4 通過 optimization 取代了4個常用的插件:
| 廢棄插件 | optimization 屬性 | 功能 | |
|---|---|---|---|
| UglifyjsWebpackPlugin | sideEffects | minimizer | Tree Shaking & Minimize |
| ModuleConcatenationPlugin | concatenateModules | Scope hoisting | 生產環(huán)境默認開啟 |
| CommonsChunkPlugin | splitChunks | runtimeChunk | OccurrenceOrder |
| NoEmitOnErrorsPlugin | NoEmitOnErrors | 編譯出現錯誤時,跳過輸出階段 | 生產環(huán)境默認開啟 |
usedExports
Webpack 將識別出它認為沒有被使用的代碼,并在最初的打包步驟中給它做標記。
//?Base?Webpack?Config?for?Tree?Shaking
const?config?=?{
?mode:?'production',
?optimization:?{
??usedExports:?true,
??minimizer:?[
???new?TerserPlugin({...})?//?支持刪除死代碼的壓縮器
??]
?}
};
package.json 的配置
用過 redux 的童鞋應該對純函數不陌生,自然也就應該了解函數式編程,函數式編程中就有副作用一說。
照顧一下不知道的同學,那什么是副作用呢?
一個函數會、或者可能會對函數外部變量產生影響的行為。
具有副作用的文件不應該做 tree-shaking,因為這將破壞整個應用程序。比如全局樣式表及全局的 JS 配置文件。
webpack 總會害怕把你要用的代碼刪除了,所以默認所有的文件都有副作用,不能被 Tree Shaking。
//?所有文件都有副作用,全都不可?tree-shaking
{
?"sideEffects":?true
}
//?沒有文件有副作用,全都可以 tree-shaking,即告知 webpack,它可以安全地刪除未用到的 export。
{
?"sideEffects":?false
}
//?除了數組中包含的文件外有副作用,所有其他文件都可以?tree-shaking,但會保留符合數組中條件的文件
{
?"sideEffects":?[
???"*.css",
???"*.less"
?]
}
所以,首先關閉你的 sideEffects,
直接通過?module.rules?中的 sideEffects 配置可縮小你的影響范圍。
加了 sideEffect 配置后,構建出來的一些 IIFE 函數也會加上/PURE/注釋,便于后續(xù) treeshaking。
組件不支持DCE?

我們的組件用的是 father,可以看到其依賴的father-build 是基于 rollup 的,那就好辦了。webpack 的 Tree Shaking 還是 copy 的 rollup家的。
關鍵是在應用組件的業(yè)務項目里面配置optimization.sideEffects: true
//?webpack.config.js
const?path?=?require('path')
const?webpackConfig?=?{
??module?:?{
????rules:?[
??????{
????????test:?/\.(jsx|js)$/,
????????use:?'babel-loader',
????????exclude:?path.resolve(__dirname,?'node_modules')
??????}???
????]
??},
optimization?:?{
??sideEffects:?true,
??minimizer:?[
????//?這里配置成空數組是為了使最終產生的?main.js?不被壓縮
??]
},
??plugins:[]
};
module.exports?=?webpackConfig;
//?package.json
{
??"name":?"treeshaking-test",
??"version":?"0.1.0",
??"description":?"",
??"main":?"src/index.js",
??"scripts":?{
????"build":?"webpack?--config?webpack.config.js"
??},
??"author":?"lu.lu??(https://github.com/lulu27753)" ,
??"license":?"MIT",
??"dependencies":?{
????"big-module":?"^0.1.0",
????"big-module-with-flag":?"^0.1.0",
????"webpack-bundle-analyzer":?"^3.7.0"
??},
??"devDependencies":?{
????"babel-preset-env":?"^1.7.0",
????"webpack":?"^4.43.0",
????"webpack-cli":?"^3.3.11"
??}
}
//?.babelrc
{
??"presets":?[
????["env",?{?"modules":?false?}]
??]
}
可以看到最終打包后的文件如下:
//?dist/main.js
"use?strict";
//?ESM?COMPAT?FLAG
__webpack_require__.r(__webpack_exports__);
//?CONCATENATED?MODULE:?./node_modules/big-module/es/a.js
var?a?=?'a';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/b.js
var?b?=?'b';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/c.js
var?c?=?'c';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/index.js
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/a.js
var?a_a?=?'a';
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/b.js
var?b_b?=?'b';
//?CONCATENATED?MODULE:?./src/index.js
console.log(a,?b,?a_a,?b_b);
/***/?})
/******/?]);
可以很清楚的看到?big-module-with-flag?中的 c 模塊被DCE了。
做個小小的改動,將?.babelrc?中的?modules?改為"commonjs"
{
??"presets":?[
????["env",?{?"modules":?"commonjs"?}]
??]
}
"use?strict";
//?ESM?COMPAT?FLAG
__webpack_require__.r(__webpack_exports__);
//?EXPORTS
__webpack_require__.d(__webpack_exports__,?"a",?function()?{?return?/*?reexport?*/?a;?});
__webpack_require__.d(__webpack_exports__,?"b",?function()?{?return?/*?reexport?*/?b;?});
__webpack_require__.d(__webpack_exports__,?"c",?function()?{?return?/*?reexport?*/?c;?});
//?CONCATENATED?MODULE:?./node_modules/big-module/es/a.js
var?a?=?'a';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/b.js
var?b?=?'b';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/c.js
var?c?=?'c';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/index.js
/***/?}),
/*?2?*/
/***/?(function(module,?__webpack_exports__,?__webpack_require__)?{
"use?strict";
//?ESM?COMPAT?FLAG
__webpack_require__.r(__webpack_exports__);
//?EXPORTS
__webpack_require__.d(__webpack_exports__,?"a",?function()?{?return?/*?reexport?*/?a;?});
__webpack_require__.d(__webpack_exports__,?"b",?function()?{?return?/*?reexport?*/?b;?});
__webpack_require__.d(__webpack_exports__,?"c",?function()?{?return?/*?reexport?*/?c;?});
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/a.js
var?a?=?'a';
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/b.js
var?b?=?'b';
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/c.js
var?c?=?'c';
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/index.js
/***/?})
/******/?]);
結果是?CDE?失??!
將?modules?的值改回去,并升級big-module-with-flag為0.2.0。CDE?成功,可以打假一波了(?,網上很多文章都是基于webpack3的,過時了)
升級big-module-with-flag為0.5.0, 并更改?src/index.js
import?{?a?as?a1,?b?as?b1?}?from?"big-module";
import?{?a?as?a2,?b?as?b2,?Apple??}?from?"big-module-with-flag";
console.log(a1,?b1,?a2,?b2);
const?appleModel?=?new?Apple({model:?'IphoneX'}).getModel()
console.log(appleModel)
var?Apple?=?/*#__PURE__*/function?()?{
??function?Apple(_ref)?{
????var?model?=?_ref.model;
????_classCallCheck(this,?Apple);
????this.className?=?'Apple';
????this.model?=?model;
??}
??_createClass(Apple,?[{
????key:?"getModel",
????value:?function?getModel()?{
??????return?this.model;
????}
??}]);
??return?Apple;
}();
//?CONCATENATED?MODULE:?./src/index.js
console.log(a,?b,?es_a,?es_b);
var?appleModel?=?new?Apple({
??model:?'IphoneX'
}).getModel();
console.log(appleModel);
DCE 成功!
var?_bigModule?=?__webpack_require__(2);
var?_bigModuleWithFlag?=?__webpack_require__(1);
console.log(_bigModule.a,?_bigModule.b,?_bigModuleWithFlag.a,?_bigModuleWithFlag.b);
var?appleModel?=?new?_bigModuleWithFlag.Apple({
??model:?'IphoneX'
}).getModel();
console.log(appleModel);
/***/?}),
/*?1?*/
/***/?(function(module,?__webpack_exports__,?__webpack_require__)?{
"use?strict";
//?ESM?COMPAT?FLAG
__webpack_require__.r(__webpack_exports__);
//?EXPORTS
__webpack_require__.d(__webpack_exports__,?"a",?function()?{?return?/*?reexport?*/?es_a;?});
__webpack_require__.d(__webpack_exports__,?"b",?function()?{?return?/*?reexport?*/?es_b;?});
__webpack_require__.d(__webpack_exports__,?"c",?function()?{?return?/*?reexport?*/?es_c;?});
__webpack_require__.d(__webpack_exports__,?"Person",?function()?{?return?/*?reexport?*/?Person;?});
__webpack_require__.d(__webpack_exports__,?"Apple",?function()?{?return?/*?reexport?*/?Apple;?});
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/a.js
var?a?=?'a';
/*?harmony?default?export?*/?var?es_a?=?(a);
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/b.js
var?b?=?'b';
/*?harmony?default?export?*/?var?es_b?=?(b);
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/c.js
var?c?=?'c';
/*?harmony?default?export?*/?var?es_c?=?(c);
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/Person.js
function?_classCallCheck(instance,?Constructor)?{?if?(!(instance?instanceof?Constructor))?{?throw?new?TypeError("Cannot?call?a?class?as?a?function");?}?}
function?_defineProperties(target,?props)?{?for?(var?i?=?0;?i?false;?descriptor.configurable?=?true;?if?("value"?in?descriptor)?descriptor.writable?=?true;?Object.defineProperty(target,?descriptor.key,?descriptor);?}?}
function?_createClass(Constructor,?protoProps,?staticProps)?{?if?(protoProps)?_defineProperties(Constructor.prototype,?protoProps);?if?(staticProps)?_defineProperties(Constructor,?staticProps);?return?Constructor;?}
var?Person?=?/*#__PURE__*/function?()?{
??function?Person(_ref)?{
????var?name?=?_ref.name,
????????age?=?_ref.age,
????????sex?=?_ref.sex;
????_classCallCheck(this,?Person);
????this.className?=?'Person';
????this.name?=?name;
????this.age?=?age;
????this.sex?=?sex;
??}
??_createClass(Person,?[{
????key:?"getName",
????value:?function?getName()?{
??????return?this.name;
????}
??}]);
??return?Person;
}();
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/Apple.js
function?Apple_classCallCheck(instance,?Constructor)?{?if?(!(instance?instanceof?Constructor))?{?throw?new?TypeError("Cannot?call?a?class?as?a?function");?}?}
function?Apple_defineProperties(target,?props)?{?for?(var?i?=?0;?i?false;?descriptor.configurable?=?true;?if?("value"?in?descriptor)?descriptor.writable?=?true;?Object.defineProperty(target,?descriptor.key,?descriptor);?}?}
function?Apple_createClass(Constructor,?protoProps,?staticProps)?{?if?(protoProps)?Apple_defineProperties(Constructor.prototype,?protoProps);?if?(staticProps)?Apple_defineProperties(Constructor,?staticProps);?return?Constructor;?}
var?Apple?=?/*#__PURE__*/function?()?{
??function?Apple(_ref)?{
????var?model?=?_ref.model;
????Apple_classCallCheck(this,?Apple);
????this.className?=?'Apple';
????this.model?=?model;
??}
??Apple_createClass(Apple,?[{
????key:?"getModel",
????value:?function?getModel()?{
??????return?this.model;
????}
??}]);
??return?Apple;
}();
//?CONCATENATED?MODULE:?./node_modules/big-module-with-flag/es/index.js
/***/?}),
/*?2?*/
/***/?(function(module,?__webpack_exports__,?__webpack_require__)?{
"use?strict";
//?ESM?COMPAT?FLAG
__webpack_require__.r(__webpack_exports__);
//?EXPORTS
__webpack_require__.d(__webpack_exports__,?"a",?function()?{?return?/*?reexport?*/?a;?});
__webpack_require__.d(__webpack_exports__,?"b",?function()?{?return?/*?reexport?*/?b;?});
__webpack_require__.d(__webpack_exports__,?"c",?function()?{?return?/*?reexport?*/?c;?});
//?CONCATENATED?MODULE:?./node_modules/big-module/es/a.js
var?a?=?'a';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/b.js
var?b?=?'b';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/c.js
var?c?=?'c';
//?CONCATENATED?MODULE:?./node_modules/big-module/es/index.js
//?.babelrc
{
??"presets":?[["env",?{?"loose":?false?}]]
}
總結
webpack 官方號稱提速 98%,其最重要的前提就是你的模塊引入方式要是ESM,而不能是因為兼容性考慮的UMD實現。
如果你是一個第三方庫的維護者,請人性化的按業(yè)界規(guī)范提供ES版本,同時配置 sideEffects: false.
Webpack 只有在壓縮代碼的時候會 tree-shaking, 通常就指是生產環(huán)境 代碼的 module 引入必須是 import 的引入方式,也就意味著被轉換成 ES5 的代碼是無法支持 tree-shaking 的。
滿足了文件要求后,簡單來說你需要做如下配置操作
[x] 在 package.json 文件中將 sideEffects 設為 false [x] 將css相關 loader中 sideEffects 設為 true [x] 讓@babel/preset-env 不編譯 ES6 模塊語句 [ ] 使用TerserPlugin,js代碼壓縮插件(webpack 自帶)
參考
webpack 官方文檔:https://webpack.docschina.org/guides/tree-shaking/
官方DEMO:https://github.com/webpack/webpack/tree/master/examples/side-effects
webpack 新插件系統(tǒng)如何工作:https://medium.com/webpack/the-new-plugin-system-week-22-23-c24e3b22e95
Tree-Shaking原理:https://juejin.im/post/5a4dc842518825698e7279a9
組件沒辦法DCE?:https://zhuanlan.zhihu.com/p/32831172


