天天看點

從過去到現在,聊聊 Tree-shaking 到底是什麼?

前言

Tree-shaking 這一術語在前端社群内,起初是 Rich Harris 在 Rollup 中提出。簡單概括起來,Tree-shaking 可以使得項目最終建構(Bundle)結果中隻包含你實際需要的代碼。

從過去到現在,聊聊 Tree-shaking 到底是什麼?

而且,說到 Tree-shaking,不難免提及 Dead Code Elimination,相信很多同學在一些關于 Tree-shaking 的文章中都會看到諸如這樣的描述:Tree-shaking 是一項 Dead Code Elimination(以下統稱 DCE)技術。

那麼,既然有了 DCE 這一術語,為什麼又要造一個 Tree-shaking 術語?存在既有價值,下面,讓我們一起來看看 Rich Harris 是如何回答這個問題的。

1 Tree-shaking Vs Dead Code Elimination

在當時 Rich Haris 針對這一提問專門寫了這篇文章《Tree-shaking versus dead code elimination》,文中表示 DCE 和 Tree-shaking 最終的目标是一緻的(更少的代碼),但是它們仍然是存在差別的。

Rich Haris 舉了個做蛋糕的例子,指出 DCE 就好比在做蛋糕的時候直接把雞蛋放入攪拌,最後在做好的蛋糕中取出蛋殼,這是不完美的做法,而 Tree-shaking 則是在做蛋糕的時候隻放入我想要的東西,即不會把蛋殼放入攪拌制作蛋糕。

是以,Tree-shaking 表達的不是指消除 Dead Code,而是指保留 Live Code。即使最終 DCE 和 Tree-shaking 的結果是一緻的,但是由于 JavaScript 靜态分析的局限性,實際過程并不同。并且,包含有用的代碼可以得到更好的結果,從表面看(做蛋糕的例子)這也是一種更符合邏輯的方法。

此外,當時 Rich Haris 也認為 Tree-shaking 可能不是一個很好的名稱,考慮過用 Live Code Inclusion 這個短語來表示,但是似乎會造成更多的困惑…讓我們看一下 Rich Haris 的原話:

I thought about using the ‘live code inclusion’ phrase with Rollup, but it seemed that I’d just be adding even more confusion seeing as tree-shaking is an existing concept. Maybe that was the wrong decision?

是以,我想到這裡同學們應該清楚一點,Tree-shaking 和 DCE 隻是最終的結果是一緻的,但是 2 者實作的過程不同,Tree-shaking 是保留 Live Code,而 DCE 是消除 Dead Code。

并且,當時 Rich Harris 也指出 Rollup 也不是完美的,最好的結果是使用 Rollup + Uglify 的方式。不過,顯然現在的 Rollup

v2.55.1

已經臻至完美。那麼,接下來讓我們沿着時間線看看 Tree-shaking 的演變~

2 Tree-shaking 的演變

Tree-shaking 在最初被提出的時候它隻會做一件事,那就是利用 ES Module 靜态導入的特點來檢測子產品内容的導出、導入以及被使用的情況,進而實作保留 Live Code 的目的。

也許這個時候你會問 Tree-shaking 不是還會消除 Dead Code 嗎?确實,但是也不一定,如果你使用的是現在的 Rollup

v2.55.1

,它是會進行 DCE,即消除 Dead Code。但是,如果你用的是 Webpack 的話,那就是另一番情況了,它需要使用 Uglify 對應的插件來實作 DCE。

下面,我們以 Rollup 為例,聊聊過去和現在的 Tree-shaking。

2.1 過去的 Tree-shaking

在早期, Rollup 提出和支援 Tree-shaking 的時候,它并不會做額外的 DCE,這也可以在 15 年 Rich Haris 寫的那篇文章中看出,當時他也提倡大家使用 Rollup + Uglify。是以,這裡讓我們一起把時間倒回 Rollup

v0.10.0

的 Tree-shaking。

回到 Rollup

v0.10.0

版本,你會發現非常有趣的一點,就是它的 GitHub README 介紹是這樣的:

從過去到現在,聊聊 Tree-shaking 到底是什麼?

Rollup 的命名來源于一首名為《Roll up》的說唱歌曲,我想這應該出乎了很多同學的意料。不過話說 Evan You 也喜歡說唱,然後我(你)也喜歡說唱,是以這也許可以論證我(你)選擇前端似乎沒錯?這裡附上這首歌,你可以選擇聽這首歌來拉近 Rollup 的距離。

傳送門:https://www.youtube.com/watch?v=UhQz-0QVmQ0

下面,我們使用 Rollup

v0.10.0

版本來做一個簡單示例來驗證一下前面說的。并且,在這個過程中需要注意,如果你的 Node 版本過高會導緻一些不相容,是以建議用 Node

v11.15.0

來運作下面的例子。

首先,初始化項目和安裝基礎的依賴:

npm init -y
npm i [email protected] -D
           

然後,分别建立 3 個檔案:

utils.js

export const foo = function () {
  console.log("foo");
};

export const bar = function () {
  console.log("bar");
};
           

main.js

import { foo, bar } from "./utils.js";

const unused = "a";

foo();
           

index.js

const rollup = require("rollup");

rollup
  .rollup({
    entry: "main.js",
  })
  .then(async (bundle) => {
    bundle.write({
      dest: "bundle.js",
    });
  });
           

其中,

main.js

是建構的入口檔案,然後

index.js

負責使用 Rollup 進行建構,它會将最終的建構結果寫入到

bundle.js

檔案中:

// bundle.js
const foo = function () {
  console.log("foo");
};

const unused = "a";

foo();
           

可以看到,在

bundle.js

中保留了

utils.js

中的

foo()

函數(因為被調用了),而導入的

uitls.js

中的

bar()

函數(沒有被調用)則不會保留,并且定義的變量

ununsed

雖然沒有被使用,但是仍然保留了下來。

是以,通過這麼一個小的示例,我們可以驗證得知 Rollup 的 Tree-shaking 最初并不支援 DCE,它僅僅隻是在建構結果中保留你導入的子產品中需要的代碼。

2.2 現在的 Tree-shaking

前面,我們從過去的 Tree-shaking 開始了解,大緻建立起了對 Tree-shaking 的初印象。這裡我們來看一下現在 Rollup 官方上對 Tree-shaking 的介紹:

Tree-shaking,也被稱為 Live Code Inclusion,是指 Rollup 消除項目中實際未使用的代碼的過程,它是一種 Dead Code Elimination 的方式,但是在輸出方面會比其他方法更有效。該名稱源自子產品的抽象文法樹(Abstract Sytanx Tree)。該算法首先會标記所有相關的語句,然後通過搖動文法樹來删除所有的 Dead Code。它在思想上類似于 GC(Garbage Collection)中的标記清除算法。盡管,該算法不限于 ES Module,但它們使其效率更高,因為它允許 Rollup 将所有子產品一起視為具有共享綁定的大抽象文法樹。

從這段話,我們可以很容易地發現随着時間的推移,Rollup 對 Tree-shaking 的定義已經不僅僅是 ES Module 相關,此外它還支援了 DCE。是以,有時候我們看到一些文章介紹 Tree-shaking 實作會是這樣:

  • 利用 ES Module 可以進行靜态分析的特點來檢測子產品内容的導出、導入以及被使用的情況,保留 Live Code
  • 消除不會被執行和沒有副作用(Side Effect) 的 Dead Code,即 DCE 過程

那麼,在前面我們已經知道 Tree-shaking 基于 ES Module 靜态分析的特點會做的事情。是以,這裡我們來仔細看一下第 2 點,換個角度看,它指的是當代碼沒有被執行,但是它會存在副作用,這個時候 Tree-shaking 就不會把這部分代碼消除。

那麼,顯然對副作用建立良好的認知,可以讓項目中代碼能更好地被 Tree-shaking。是以,下面讓我們來通過一個簡單的例子來認識一下副作用。

2.2.1 副作用(Side Effect)

在 Wiki 上對副作用(Side Effect)做出的介紹:

在計算機科學中,如果操作、函數或表達式在其本地環境之外修改某些狀态變量值,則稱其具有副作用。

把這段話轉換成我們熟悉的,它指的是當你修改了不包含在目前作用域的某些變量值的時候,則會産生副作用。這裡我們把上面的例子稍作修改,把

sayHi()

函數的形參删掉,改為直接通路定義好的

name

變量:

utils.js

export const name = "wjc";

export const sayHi = function () {
  console.log(`Hi ${name}`);
};
           

main.js

import { sayHi } from "./maths.js";

sayHi();
           

然後,我們把這個例子通過 Rollup 提供的 REPL 來 Tree-shaking 一下,輸出的結果會是這樣:

const name = "wjc";

const sayHi = function () {
  console.log(`Hi ${name}`);
};

sayHi();
           

可以看到,這裡我們并沒有直接導入

utils.js

檔案中的

name

變量,但是由于在

sayHi()

函數中通路了它作用域之外的變量

name

,産生了副作用,是以最後輸出的結果也會有

name

變量。

當然,這僅僅隻是一個非常簡單的産生副作用的場景,也是很多同學不會犯的錯誤。此外,一個很有趣的場景就是使用

Class

關鍵字聲明的類經過 Babel 轉換為 ES5 的代碼(為了保證

Class

可枚舉)後會産生副作用。

對上面提到的這個問題感興趣的同學,可以看這篇文章 你的 Tree-Shaking 并沒什麼用 仔細了解,這裡就不做重複論述了~

結語

寫這篇文章的動機主要是出于對 Tree-shaking 和 DCE 這兩個術語十分相似,但是 Tree-shaking 必然有其存在的意義,是以就誕生了這篇文章。雖然,文章中并沒有涉及 Tree-shaking 的底層實作,但是我想有時候搞清楚一些模糊的概念的優先級是優于了解其底層實作的。

并且,通過對比 2015 年 Rich Harris 在提出 Tree-shaking 的初衷,到現在 Tree-shaking 所具備的能力來說,随着時間的演變 Rollup 的 Tree-shaking 預設也支援了 DCE,這也難免會造成一些同學對 Tree-shaking 的了解産生混亂。是以,如果想要追溯本源(Tree-shaking 由來)的同學,我還是蠻推薦仔細閱讀一下《Tree-shaking versus dead code elimination》這篇文章的。

最後,如果文中存在表達不當或錯誤的地方,歡迎各位同學提 Issue ~

點贊

通過閱讀本篇文章,如果有收獲的話,可以點個贊,這将會成為我持續分享的動力,感謝~

我是五柳,喜歡創新、搗鼓源碼,專注于源碼(Vue 3、Vite)、前端工程化、跨端等技術學習和分享,歡迎關注我的微信公衆号:Code center。
從過去到現在,聊聊 Tree-shaking 到底是什麼?