所謂Tree-shaking就是‘搖’的意思,做用是把項目中不必的模塊所有抖掉,用於在不一樣的模塊之間消除無用的代碼,可列爲性能優化的範疇。javascript
Tree-shaking早期由rollup實現,後來webpack2也實現了Tree-shaking的功能,可是至今還不是很完備。至於爲何不完備,能夠看一下百度外賣的Tree-shaking原理java
Tree-shaking的本質用於消除項目一些沒必要要的代碼。早在編譯原理中就有提到DCE(dead code eliminnation),做用是消除不可能執行的代碼,它的工做是使用編輯器判斷出某些代碼是不可能執行的,而後清除。webpack
Tree-shaking一樣的也是消除項目中沒必要要的代碼,可是和DCE又有略不相同。能夠說是DCE的一種實現,它的主要工做是應用於模塊間,在打包過程當中抽出有用的部分,用於完成DCE。git
Tree-shaking是依賴ES6模塊靜態分析的,ES6 module的特色以下:es6
依賴關係肯定,與運行時無關,靜態分析。正式由於ES6 module的這些特色,才讓Tree-shaking更加流行。github
主要特色仍是依賴於ES6的靜態分析,在編譯時肯定模塊。若是是require,在運行時肯定模塊,那麼將沒法去分析模塊是否可用,只有在編譯時分析,纔不會影響運行時的狀態。web
webpack從第2版本就開始支持Tree-shaking的功能,可是至今也並不能實現的那麼完美。凡是具備反作用的模塊,webpack的Tree-shaking就歇菜了。編程
反作用在咱們項目中,也一樣是頻繁的出現。知道函數式編程的朋友都會知道這個名詞。所謂模塊(這裏模塊可稱爲一個函數)具備反作用,就是說這個模塊是不純的。這裏能夠引入純函數的概念。瀏覽器
對於相同的輸入就有相同的輸出,不依賴外部環境,也不改變外部環境。性能優化
符合上述就能夠稱爲純函數,不符合就是不純的,是具備反作用的,是可能對外界形成影響的。
webpack自身的Tree-shaking不能分析反作用的模塊。以lodash-es這個模塊來舉個例子
//test.js
import _ from "lodash-es";
const func1 = function(value){
return _.isArray(value);
}
const func2 = function(value){
return value=null;
}
export {
func1,
func2,
}
//index.js
import {func2} from './test.js'
func2()
複製代碼
上述代碼在test.js中引入lodash-es,在func1中使用了loadsh,而且這裏不符合純函數的概念,它是具備反作用的。func2是一個純函數。
在index.js中只引入了func2,而且使用了func2,可見整個代碼的執行是和func1是沒有任何關係的。咱們經過生產環境打包一下試試看(Tree-shaking只在生產環境生效)
webpack-deep-scope-plugin是一位中國同胞(學生)在Google夏令營,在導師Tobias帶領下寫的一個webpack插件。(此時慢慢的羨慕)
這個插件主要用於填充webpack自身Tree-shaking的不足,經過做用域分析來消除無用的代碼。
這個插件是基於做用域分析的,那麼都有什麼樣的做用域?
// module scope start
// Block
{ // <- scope start
} // <- scope end
// Class
class Foo { // <- scope start
} // <- scope end
// If else
if (true) { // <- scope start
} /* <- scope end */ else { // <- scope start
} // <- scope end
// For
for (;;) { // <- scope start
} // <- scope end
// Catch
try {
} catch (e) { // <- scope start
} // <- scope end
// Function
function() { // <- scope start
} // <- scope end
// Scope
switch() { // <- scope start
} // <- scope end
// module scope end
複製代碼
對於ES6模塊來講,上面做用域只有function和class是能夠被導出的,其餘的做用域能夠稱之爲function和class的子做用域並不能被導出實際上歸屬於父做用域的。
插件經過分析代碼的做用域,進而獲得做用域與做用域之間的關係。
分析代碼的做用域的基礎是創建作AST(Abstract Syntax Tree)抽象語法樹上面的。這個能夠經過escope來完成。
拿到解析完的AST抽象語法樹,利用圖的深度優先遍歷找到哪些做用域是能夠被使用到的,哪些做用域是不能夠被使用到的。從而分析做用域之間的關係和導出變量之間的關係。進而執行模塊消除。
JavaScript中仍是有一些代碼是不會消去的。
import { isNull } from 'lodash-es';
export function scope(...args) {
return isNull(...args);
}
複製代碼
在根做用域引用到的做用域不會被消除。
import _ from "lodash-es";
var func1
func1 = function(value){
return _.isArray(value);
}
const func2 = function(value){
return value=null;
}
export {
func1,
func2,
}
複製代碼
上述代碼中先定義了func1,而後又給func1賦值,這樣缺乏了數據流分析,一樣插件也是不能夠的。
引用做者的例子
import _curry1 from './internal/_curry1';
import curryN from './curryN';
import max from './max';
import pluck from './pluck';
var allPass = /*#__PURE__*/_curry1(function allPass(preds) {
return curryN(reduce(max, 0, pluck('length', preds)), function () {
var idx = 0;
var len = preds.length;
while (idx < len) {
if (!preds[idx].apply(this, arguments)) {
return false;
}
idx += 1;
}
return true;
});
});
export default allPass;
複製代碼
當一個匿名函數被包在一個函數調用中(IIFE也是如此),那麼插件是沒法分析的。可是若是加上/*#__PURE__*/註釋的話,這個插件會把這個函數調用看成一個獨立的域,tree-shaking是能夠生效的。
咱們都知道在這個ES6氾濫的時代,ES6的代碼在項目中出現已經很普遍。(先不考慮線上環境打包成ES5)。上面提到插件的利用做用域來分析。能導出的做用域只有class和funciton。function的狀況在上面已經說過,如今來探討一下class的狀況。
當不使用插件的時候,咱們來看一下會不會Tree-shaking,預期是會被Tree-shaking。書寫下面這樣一段簡單的代碼。
class Test{
init(value){
console.log('test init');
}
}
export {
Test,
}
複製代碼
當咱們在不適用插件的狀況下,而且引入反作用,觀察一下會不會打包,預期是不會打包。書寫下面代碼。
class Test{
init(value){
console.log('test init');
return _.isArray(value);
}
}
export {
Test,
}
複製代碼
當咱們使用插件而且代碼中存在反作用的狀況下,觀察打包狀況。因爲上面的插件原理的鋪墊,咱們預期此次是能夠Tree-shaking的。利用上例代碼來測試。
因爲用戶瀏覽器對ES6支持度不夠的緣由,線上的代碼不能全是ES6的,有時候咱們要把ES6的代碼打包成ES5的,放到線上環境來執行。利用上例代碼來測試。
??? 什麼鬼,我沒有用到它,爲何這麼大??? 一串懵逼
懵逼懵逼,babel成就了線上生產環境,但失去了Tree-shaking優化。咱們來看看怎麼回事。
當去除調反作用的時候咱們來打包一下。
"use strict";
function _instanceof(left, right) {
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || 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 Test =
/*#__PURE__*/
function () {
function Test() {
_classCallCheck(this, Test);
}
_createClass(Test, [{
key: "init",
value: function init(value) {
console.log("test init")
}
}]);
return Test;
}();
複製代碼
上面能夠看到最新的babel和webpack有了契合,在Test當即執行函數的地方使用了 /*#__PURE__*/(忘記能夠往上看),讓下面的IIFE變成可分析的,成功了使用了Tree-shaking。
上面探討狀況的時候就得知有反作用的狀況下,不能夠被打包的。ES6編譯代碼以下。
"use strict";
function _instanceof(left, right) {
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || 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 Test =
/*#__PURE__*/
function () {
function Test() {
_classCallCheck(this, Test);
}
_createClass(Test, [{
key: "init",
value: function init(value) {
console.log("test init")
return _.isArray(value);
}
}]);
return Test;
}();
複製代碼
這裏雖然bable新版契合了webpack,可是仍是有一些問題。本身也沒有找出是哪裏除了問題,做者說JavaScript代碼仍是有一些是不能夠清除的,也許就出現到這裏。提供一個做者的插件Demo。
不管是ES6,仍是ES5,Tree-shaking不能生效的緣由總的歸根結底仍是由於代碼反作用的問題。可想而知代碼的書寫規範是多麼重要。這裏我所想出的解決方案有兩種。
書寫代碼過程當中儘可能使用純函數的方式來寫代碼,保持書寫規範,不讓代碼有反作用。例如把class類引用的反作用改爲純的。
class Test{
init(value,_){ //參數引入lodash模塊
console.log('test init');
return _.isArray(value);
}
}
export{
Test,
}
複製代碼
兩套代碼。當瀏覽器支持的時候,就使用ES6的代碼,ES5的代碼。此方案可參考瀏覽器支持ES6的最優解決方案
項目中不免會一些用不到的模塊佔位置影響咱們的項目,Tree-shaking的出現也爲開發者在性能優化方面提供了很是大的幫助,靈活使用Tree-shaking才能讓Tree-shaking發揮做用,處理好項目中代碼的反作用可使項目更加的完美。