深刻理解Webpack tree shaking

tree shaking 是什麼

首先咱們先搞清楚,tree shaking是個什麼東西,來看下 wiki 給的介紹:css

In computing, tree shaking is a dead code elimination technique that is applied when optimizing code written in ECMAScript dialects like Dart, JavaScript, or TypeScript into a single bundle that is loaded by a web browserreact

翻譯過來,大概意思就是:在計算機中,搖樹是一種死代碼消除技術,用來優化由Dart、JavaScript或TypeScript等語言編寫的由web瀏覽器加載的單個包的應用。webpack

何謂「死代碼」?那就是程序運行時執行不到或者說用不到的代碼,若是是基於JS模塊化開發,最經典的例子就是若是咱們引用了 lodash 這樣的庫,可是咱們在項目中其實只用到了比較少的 utils,可是構建工具通常會把整個包打包到最終生成的JS bundle。這時候,tree shaking就能發揮極大的做用了。git

treeshaker 的概念起源於20世紀90年代的LISP,其表達的思想是:一個程序的全部可能的執行流均可以表示爲函數調用樹,這樣那些從未被調用的函數就能夠經過必定的技術手段被消除。那麼爲何在 JavaScript 中最近幾年纔出現 」tree shaking「 這項技術?github

tree shaking的發展和ES6 Module帶來的契機

咱們知道 JavaScript 是一門動態的語言,而動態語言中的死代碼消除是一個比靜態語言更難的問題。可是其實在早期也有牛逼的團隊開始作這方面的嘗試,在2012年的時候,該算法應用於Google Closure Tools 中的JavaScript,而後也應用於一樣由谷歌編寫的dart2js編譯器中的 Dart 語言。2013年,做者Chris Buckett在《Dart in Action》一書中說到:web

當代碼從Dart轉換爲JavaScript時,編譯器會執行「搖樹」操做。在JavaScript中,即便只須要一個函數,也必須添加整個庫,可是因爲樹抖動,Dart 派生的JavaScript只包含庫中須要的單個函數。算法

tree shaking下一波流行要歸功於 Rich Harris 在2015年開發的 Rollup 項目,Rich Harris 是著名的開源大神,Rollupsvelte 做者。編程

隨着ES6的出現,ES6 中的模塊化方案成爲了將來 JS 的標準,也標誌着 JS 正式邁入模塊化編程的時代。 ES6 Module 是一種能夠作靜態分析的模塊機制,這使得 tree shaking 的技術成爲了打包工具不可缺乏的技術。事實上,當前主流的tree shaking 技術依賴於 ES6 中的 importexport 模塊機制, 打包器會檢測代碼中的模塊是否被導出、導入,且被 JavaScript 文件使用。json

Webpack 中的tree shaking

Weback2的正式版已經開始支持 ES6 模塊語法(也叫作 harmony modules),其中也包含了 dead code 檢測能力,webpack4正式版本擴展和增強了 tree shaking 技術。數組

sideEffects

怎麼可以讓 Webpack 知道你項目的模塊或者指定的模塊都是 ES6 Module ,可讓 Webpack 在構建的時候放心消除 dead code?其中一種方式是經過往 package.json 中添加 sideEffects 屬性,將其值設置爲 false,來告知 webpack,項目中都是 」pure「(純正 ES6 模塊),能夠安全地刪除未用到的 export。

若是咱們想要告訴 webpack 有些文件有反作用,不能 shaking 掉的,咱們能夠指定一個數組,例如:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}
複製代碼

來告訴 webpack 這些文件不能優化。數組方式支持相對路徑、絕對路徑和 glob 模式匹配相關文件。b包含在數組中的文件將不會受到 tree shaking 的影響,由於默認狀況下,全部導入文件都會受到 tree shaking 的影響。這意味着,若是在項目中使用相似 css-loader 並 import 一個 CSS 文件,則須要將其添加到 side effect 列表中,以避免在生產模式中無心中將它刪除:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css"
  ]
}
複製代碼

你也能夠經過 module.rules 配置選項設置 sideEffects,具體查看文檔module.rules

usedExports

除了 sideEffects ,咱們也能夠經過配置 usedExports 屬性提示 webpack 作 tree shaking 優化。例如:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'development',
  optimization: {
    usedExports: true,
  },
};
複製代碼

sideEffectsusedExports 是兩種不一樣的優化方式,可是 sideEffects 更有效,是由於它容許跳過整個模塊/文件和整個文件子樹,使得優化更有效率。usedExports 依賴於 terser(一個適用於ES6+的JavaScript解析器、壓縮和優化工具包) 去檢測語句中的反作用。它是一個 JavaScript 任務並且沒有像 sideEffects 同樣簡單直接。

看一個官網的例子

雖然 usedExports 在分析 export 函數通常沒有問題,但 React 框架的高階函數(HOC)在這種狀況下是會出問題的。

咱們看個例子:

import { Button } from '@shopify/polaris';
複製代碼

打包前的文件版本看起來是這樣的:

import hoistStatics from 'hoist-non-react-statics';

function Button(_ref) {
  // ...
}

function merge() {
  var _final = {};

  for (var _len = arguments.length, objs = new Array(_len), _key = 0; _key < _len; _key++) {
    objs[_key] = arguments[_key];
  }

  for (var _i = 0, _objs = objs; _i < _objs.length; _i++) {
    var obj = _objs[_i];
    mergeRecursively(_final, obj);
  }

  return _final;
}

function withAppProvider() {
  return function addProvider(WrappedComponent) {
    var WithProvider =
    /*#__PURE__*/
    function (_React$Component) {
      // ...
      return WithProvider;
    }(Component);

    WithProvider.contextTypes = WrappedComponent.contextTypes ? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes) : polarisAppProviderContextTypes;
    var FinalComponent = hoistStatics(WithProvider, WrappedComponent);
    return FinalComponent;
  };
}

var Button$1 = withAppProvider()(Button);

export {
  // ...,
  Button$1
};
複製代碼

若是 Button 沒有被使用,工具能夠有效地清除掉 export { Button$1 },且保留全部剩下的代碼。 可是問題來了,剩下的代碼能被清理掉嗎或者它們有反作用嗎?這不太好說。尤爲是 withAppProvider()(Button) 這段代碼。withAppProvider 被調用,並且返回的值也被調用。當調用 merge 或 hoistStatics 會有任何反作用嗎?當給 WithProvider.contextTypes (Setter?) 賦值或當讀取 WrappedComponent.contextTypes (Getter) 的時候,會有任何反作用嗎?

儘管 Terser 嘗試去解決上面的問題,可是大多數狀況,它不肯定。這不是說 terser 因爲沒法解決這些問題而應用得很差,而是因爲在 JavaScript 這種動態語言中實在很難去肯定。

咱們能夠經過添加 /*#__PURE__*/ 註釋來幫助 Terser,前面這個註釋告訴 Terser,這個調用是沒有反作用的,可使用 tree shaking 優化。

var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
複製代碼

這樣的標記,會容許 Terser 移除這段代碼,可是可能還會有一些導入的問題須要評估,由於它們包含了反作用。

爲了更好解決上面這樣的問題,能夠直接使用 sideEffects 屬性。雖然它的功能相似於 /*#__PURE__*/,可是它是做用於模塊層面,而不是代碼語句的層面。這個屬性告訴 webpack:被標記爲無反作用的模塊若是沒有被直接導出使用,那就跳過對該模塊的反作用的分析評估。

在一個 Shopify Polaris 的例子,原有的模塊以下:

index.js

import './configure';
export * from './types';
export * from './components';
複製代碼

components/index.js

export { default as Breadcrumbs } from './Breadcrumbs';
export { default as Button, buttonFrom, buttonsFrom, } from './Button';
export { default as ButtonGroup } from './ButtonGroup';
複製代碼

package.json

// ...
"sideEffects": [
  "**/*.css",
  "**/*.scss",
  "./esnext/index.js",
  "./esnext/configure.js"
],
// ...
複製代碼

上述的優化,其它的項目均可以應用。例如:從 Button.js 導出 的buttonFrom 和 buttonsFrom 也沒有被使用。usedExports 優化會保留這些代碼並且 terser 可以從 bundle 中把這些語句挑選出來。模塊合併也會被應用,因此這4個模塊,加上入口的模塊(也可能有更多的依賴)會被合併。

將函數調用標記爲無反作用

咱們一樣能夠經過 /*#__PURE__*/ 告訴 webpack 某個函數調用是無反作用的,註釋通常放在函數調用以前。例如:

/*#__PURE__*/ add(55, 45);
複製代碼

固然傳入到函數中的參數是沒法被剛纔的註釋所標記,須要單獨每個標記才能夠。若是想要清理一些未被使用的變量,其實這也算是一種 dead code,webpack 有其它的配置來完成這項優化,具體能夠查看optimization.innerGraph,這裏就再也不展開。

總結

文章從 tree shaking的發展歷史到 webpack 中的 tree shaking 的具體使用以及一些須要注意的坑全面講解了 webpack tree shaking 技術的強大。最後咱們得出結論,若是想要你的項目利用好這項技術,你須要注意:

  • 使用 ES2015 模塊語法(即 import 和 export)。
  • 確保沒有編譯器將你項目中的 ES2015 模塊語法轉換爲 CommonJS 的(這是如今經常使用的 @babel/preset-env 的默認行爲,詳細信息請參閱文檔)。
  • 在項目的 package.json 文件中,添加 "sideEffects" 屬性。
  • 使用 mode 爲 "production" 的配置項以啓用更多優化項,包括壓縮代碼與 tree shaking。

若是把應用程序的源碼當作一棵樹,那麼綠色的樹葉表明的是實際使用到的源碼,也就是樹上還活着的樹葉。而棕色的樹葉表明 dead code,是秋天樹上枯萎的樹葉。爲了把枯萎的樹葉從樹上除去,就須要搖動這棵樹,此即 tree shaking 的類比。

Reference

相關文章
相關標籤/搜索