時下火熱的 Vue.js 3.0 從源碼、性能和語法 API 三個大的方面對框架進行了優化。其中,在性能的優化中,對於源碼體積的優化,主要體如今移除了一些冷門 Feature(好比 filter、inline-template) 並引入了 Tree-Shaking 減小打包體積。自從 rollup 提出這個術語以來,往往談及打包性能優化,幾乎都有 Tree-Shaking 的一席之地,因此瞭解 Tree-Shaking 的原理是頗有必要的。html
閱讀完本文,你能夠 Get 如下問題的答案,node
業界知名的模塊打包器 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,也叫死碼,無用代碼,它的範疇主要包含了如下兩點,github
咱們嘗試經過一些 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 Code
從 AST
(抽象語法樹)中刪除,但 JavaScript 是動態語言,編譯器不能幫助咱們完成死碼消除,咱們須要本身實現 Dead code elimination
。
而咱們說的 Tree-Shaking,就是 Dead code elimination 的一種實現,它藉助於 ECMAScript 6 的模塊機制原理,更多關注的是對無用模塊的消除,消除那些引用了但並無被使用的模塊。
這裏爲了更好地理解 Tree-Shaking 的原理,咱們須要先了解 ES6 的模塊機制。
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 最大的特色就是靜態化,在編譯時就能肯定模塊的依賴關係,以及輸入和輸出的值,這意味着什麼?意味着模塊的依賴關係是肯定的,和運行時的狀態無關,能夠進行可靠的靜態分析,正是基於這個基礎,才使得 Tree-Shaking 成爲可能,這也是爲何 rollup 和 Webpack 2 都要用 ES6 Module 語法才能支持 Tree-Shaking。
瞭解原理後,接下來咱們來看下如何實現 Tree-Shaking。
藉助靜態模塊分析,Tree-Shaking 實現的大致思路:藉助 ES6 模塊語法的靜態結構,經過編譯階段的靜態分析,找到沒有引入的模塊並打上標記,而後在壓縮階段利用像 uglify-js
這樣的壓縮工具刪除這些沒有用到的代碼。
是這樣嗎?接下來咱們以 webpack
爲例,驗證下上述思路。
初始化項目安裝最新的 webpack
和 webpack-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.js
中 math
模塊打包後代碼,
/***/ "./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,對做者也是一種鼓勵
(完)