Tree Shaking:從原理到實現

時下火熱的 Vue.js 3.0 從源碼、性能和語法 API 三個大的方面對框架進行了優化。其中,在性能的優化中,對於源碼體積的優化,主要體如今移除了一些冷門 Feature(好比 filter、inline-template) 並引入了 Tree-Shaking 減小打包體積。自從 rollup 提出這個術語以來,往往談及打包性能優化,幾乎都有 Tree-Shaking 的一席之地,因此瞭解 Tree-Shaking 的原理是頗有必要的。html

閱讀完本文,你能夠 Get 如下問題的答案,node

  • 什麼是 Tree-Shaking?Tree-Shaking 的發展歷史?
  • Tree-Shaking 的原理是什麼?
  • 什麼是 Dead code?
  • ECMAScript 6 的模塊機制?
  • Webpack 中 Tree-Shaking 的原理?

故事的開始:Rich Harris 和他的 Rollup

業界知名的模塊打包器 rollup.js 的做者 Rich Harris 在 2015 年 12 月的一篇博客 《Tree-shaking versus dead code elimination》中首次提到了 Tree-Shaking 的概念,隨後 Webpack 2 的正式版本內置支持了 ECMAScript 2015 模塊,增長了對 Tree-Shaking 的支持,而在更早前,Google 推出的開發者工具 Closure Compiler 也在作相似的事情。webpack

I’ve been working (albeit sporadically of late, admittedly) on a tool called Rollup, which bundles together JavaScript modules. One of its features is tree-shaking, by which I mean that it only includes the bits of code your bundle actually needs to run.git

Rich Harris 在文中提到 Tree-Shaking 是爲了 Dead code elimination,這是編譯器原理中常見的一種編譯優化技術,簡單來講就是消除無用代碼(Dead code)。那麼什麼是 Dead code 呢?es6

Dead code

Dead code,也叫死碼,無用代碼,它的範疇主要包含了如下兩點,github

  1. 不會被運行到的代碼(unreachable code)
  2. 只會影響道無關程序運行結果的變量(Dead Variables)

咱們嘗試經過一些 JavaScript 代碼片斷來理解它。web

首先,不會被運行到的代碼很好理解,好比在函數 return 語句後的代碼,npm

function foo() {
  return 'foo';
  var bar = 'bar'; // 函數已經返回了,這裏的賦值語句永遠不會執行
}
複製代碼

或者不會執行的假值條件語句塊,編程

if(0) {
  // 這個條件判斷語句塊內部的代碼永遠不會執行
}
複製代碼

Dead Variables 常見的像一些未使用的變量,api

function add(a, b) {
  let c = 1; // unused variable 在這裏能夠被看做死碼
  return a + b;
}
複製代碼

須要注意的是,模塊若是未使用也能夠看做 Dead code,好比下面的 bar 模塊,

// foo.js
function foo() {
  console.log('foo');
}
export default foo;

// bar.js
function bar() {
  console.log('bar');
}
export default bar;

// index.js
import foo from './foo.js';
import bar from './bar.js';
foo();

// 這裏入口文件雖然引用了模塊 bar,可是沒有使用,模塊 bar 也能夠被看做死碼
複製代碼

Dead code 咱們知道了,那麼什麼是 Tree-Shaking 呢?

在傳統的靜態編程語言編譯器中,編譯器能夠判斷出某些代碼根本不影響輸出,咱們能夠藉助編譯器將 Dead CodeAST(抽象語法樹)中刪除,但 JavaScript 是動態語言,編譯器不能幫助咱們完成死碼消除,咱們須要本身實現 Dead code elimination

咱們說的 Tree-Shaking,就是 Dead code elimination 的一種實現,它藉助於 ECMAScript 6 的模塊機制原理,更多關注的是對無用模塊的消除,消除那些引用了但並無被使用的模塊。

這裏爲了更好地理解 Tree-Shaking 的原理,咱們須要先了解 ES6 的模塊機制。

ECMAScript 6 module

JavaScript 的模塊化經歷一個漫長的發展歷程,咱們知道剛開始 JavaScript 是沒有模塊的概念的,最初咱們只能藉助 IIFE 儘可能減小對全局環境的污染,後來社區出現了用於瀏覽器端的以 RequireJS 爲表明的 AMD 規範和以 Sea.js 爲表明的 CMD 規範,服務器端也出現了 CommonJS 規範,再後來 JavaScript 原生引入了 ES Module,取代社區方案成爲瀏覽器端一致的模塊解決方案。

ES Module 如今也能夠用於服務器,Node.js 在 v12.0.0 版本實現了對 ES Module 的支持。

對於 ES Module 基礎語法不瞭解的能夠參考下面的文章,咱們接下來主要理解它的機制,

對比是理解知識很是有效的一種手段。咱們經過對比 ES Module 與 CommonJS 的區別來理解 ES Module 的模塊機制,它們的區別主要體如今模塊的輸出和執行上,

  • ES Module 輸出的是值的引用,而 CommonJS 輸出的是值的拷貝
  • ES Module 是編譯時執行,而 CommonJS 模塊是在運行時加載

因此 ES Module 最大的特色就是靜態化,在編譯時就能肯定模塊的依賴關係,以及輸入和輸出的值,這意味着什麼?意味着模塊的依賴關係是肯定的,和運行時的狀態無關,能夠進行可靠的靜態分析,正是基於這個基礎,才使得 Tree-Shaking 成爲可能,這也是爲何 rollup 和 Webpack 2 都要用 ES6 Module 語法才能支持 Tree-Shaking。

瞭解原理後,接下來咱們來看下如何實現 Tree-Shaking。

Tree Shaking

藉助靜態模塊分析,Tree-Shaking 實現的大致思路:藉助 ES6 模塊語法的靜態結構,經過編譯階段的靜態分析,找到沒有引入的模塊並打上標記,而後在壓縮階段利用像 uglify-js 這樣的壓縮工具刪除這些沒有用到的代碼。

是這樣嗎?接下來咱們以 webpack 爲例,驗證下上述思路。

初始化項目安裝最新的 webpackwebpack-cli ,筆者寫這篇文章時最新的版本是 v5.35.1

$ mkdir tree-shaking && cd tree-shaking
$ npm init -y
$ npm i webpack webpack-cli -D
複製代碼

添加一個簡單的配置文件和一個 math 模塊,這裏咱們只引用 math 模塊的 cube 函數,

// webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  optimization: {
    // 開啓 usedExports 收集 Dead code 相關的信息
    usedExports: true,
  },
};

// src/math.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  var a, b, c; // 這裏引入了三個未使用的變量做爲 Dead code 的一種
  return x * x * x;
}

// src/index.js
import { cube } from "./math.js";

function component() {
  var element = document.createElement("pre");
  element.innerHTML = "5 cubed is equal to " + cube(5);
  return element;
}

document.body.appendChild(component());
複製代碼

運行打包命令,定位到 bundle.jsmath 模塊打包後代碼,

/***/ "./src/math.js":
/*!*********************!*\ !*** ./src/math.js ***! \*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"cube\": () => (/* binding */ cube)\n/* harmony export */ });\n/* unused harmony export square */\nfunction square(x) {\r\n return x * x;\r\n}\r\n\r\nfunction cube(x) {\r\n var a, b, c;\r\n return x * x * x;\r\n}\r\n\n\n//# sourceURL=webpack://tree-shaking/./src/math.js?");

/***/ })
複製代碼

爲了方便閱讀,咱們將 eval 函數內的換行符去掉,簡單整理下格式,

/* harmony export */
__webpack_require__.d(__webpack_exports__, {
  /* harmony export */
  cube: () => /* binding */ cube /* harmony export */,
});
/* unused harmony export square */
function square(x) {
  return x * x;
}
function cube(x) {
  var a, b, c;
  return x * x * x;
}
複製代碼

能夠看到,__webpack_exports__ 只導出了 cube 函數,而沒有使用的 square 函數沒有被導出,並打上了 /* unused harmony export square */ 的註釋標識,可是 square 函數聲明以及 cube 函數中未使用的變量聲明 a, b, c 仍是被打包了。這印證了咱們以前推測的 webpack 能夠經過 Tree-Shaking 找到沒有引入的模塊,並不會刪除 Dead code。

接着咱們將 mode 切換到 production 以啓用 uglify-js 進行壓縮,而後再次運行打包命令,

(() => {
  "use strict";
  var e, t;
  document.body.appendChild(
    (((t = document.createElement("pre")).innerHTML =
      "5 cubed is equal to " + (e = 5) * e * e),
    t)
  );
})();

複製代碼

結果和咱們預期一致,uglify-js 在壓縮的同時去除了 Dead code,包括,

  • 沒有使用的 square 函數
  • 沒有使用的變量 a, b, c

咱們也能夠單獨引入 uglify-js 來驗證這一點,

// math.js
function cube(x) {
  var a, b, c;
  return x * x * x;
}

// minify.js
const fs = require("fs");
const UglifyJS = require("uglify-js");
const code = fs.readFileSync("math.js", "utf-8");
const result = UglifyJS.minify(code, {
  compress: {
    dead_code: true, // dead_code 默認爲 true
  },
});

console.log(result.code); // function cube(n){return n*n*n}
複製代碼

小結

咱們從 Tree-Shaking 的起源切入,瞭解了它是 Dead code elimination 的一種實現,而後拓展學習了什麼是 Dead Code,接着回顧了 JavaScript 模塊化的發展史,正是由於 ES Module 的靜態結構,使得模塊級別的 Tree-Shaking 實現成爲可能。最後以打包工具 webpack 爲例,結合 uglify-js 壓縮工具,解釋了 Tree-Shaking 的實現原理。

參考連接

寫在最後

本文首發於個人 博客,才疏學淺,不免有錯誤,文章有誤之處還望不吝指正!

若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤

若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵

(完)

相關文章
相關標籤/搜索