您好,登錄后才能下訂單哦!
這篇文章主要講解了JavaScript打包壓縮工具的用法,內(nèi)容清晰明了,對此有興趣的小伙伴可以學(xué)習(xí)一下,相信大家閱讀完之后會有幫助。
背景
平時大家在開發(fā) Js 項(xiàng)目的時候,可能已經(jīng)離不開 webpack 等打包工具了。而 webpack 打包速度大概就是“能用“的水平。大概去年開始,我就開始在構(gòu)想,如果能寫一個極速的打包工具,功能未必需要很強(qiáng),可能對小項(xiàng)目非常有用。去年我用 C++ 寫完 parser 之后,便沒什么動力寫下去了。但是最近發(fā)現(xiàn)有這個想法的不止我一個,F(xiàn)igma 的 CTO 業(yè)余之際寫了一個打包器 https://github.com/evanw/esbuild ,可以說完完全全實(shí)現(xiàn)了我想象中的需求,不過他是用 Go 語言實(shí)現(xiàn)的。我看到這個項(xiàng)目時心里一想,這不是我去年就想做的事嗎,這 push 我趕緊把打包壓縮部分完成。
代碼
Github 地址: https://github.com/vincentdchan/jetpack.js
優(yōu)化思路
并行 Parsing
毫無疑問,每一個 js 文件的 parsing 可以在不同線程完成,這就需要支持并行的語言。由于 parsing 的結(jié)果是 AST,所以需要可以共享內(nèi)存的語言(排除通過 messeage parsing 實(shí)現(xiàn)多線程的語言)。滿足以上兩個要求的語言不多。 Evan 選擇了 Go,我選擇了 C++。
減少遍歷次數(shù)
要想速度快,就要減少 AST 的遍歷次數(shù)。最好就是只遍歷一次來生成代碼,在 Parsing 構(gòu)建 AST 的時候就收集足夠的信息。但是這也意味著只能做比較淺層次的優(yōu)化,不能做深層次的壓縮(死代碼消除,tree shaking 都做不了)。
架構(gòu)
由上述思路我總結(jié)出了以下打包的架構(gòu):
流程圖如下:
打包壓縮原理
本章節(jié)主要講如何“最簡單“地壓縮 Js 代碼。本章節(jié)假設(shè)讀者對編譯原理有一定了解,知道什么是 AST。如果不懂請直接跳到下文「性能」章節(jié)。
字面量替換
字面替換最簡單。規(guī)則有一下幾個:
void 0
!0
, false 替換為 !1
:warning: 注意:在 ES 中,undefined 是標(biāo)識符(Identifier),而不是關(guān)鍵字,也就是說你可以定義一個叫 undefined 的變量,所以這個時候不能簡單地替換為 void 0
常量折疊
計(jì)算簡單的運(yùn)算:
var two = 1 + 1; var foobar = 'foo' + 'bar';
轉(zhuǎn)換成
var two = 2; var foobar = 'foobar';
:warning: 注意:這里要注意實(shí)現(xiàn)的平臺和 js 的差異,比如在 C++ 里面大整數(shù)相加可能會溢出,而在 Js 會自動轉(zhuǎn)換成 bigint. 加法問題就如此,其他運(yùn)算符問題更多。如果要完整實(shí)現(xiàn)常量折疊,可能要部分實(shí)現(xiàn) js 引擎。
變量別名
別名就是要給變量重新賦予比較短的變量名。從字母一直排上去,abcd,一個字母用完了用兩個字母。實(shí)現(xiàn)起來也很簡單,用一個計(jì)數(shù)器,一直加上去就可。最后每個變量分配一個數(shù)字,把這個數(shù)字映射到相應(yīng)的英文字母上,有點(diǎn)像 36 進(jìn)制轉(zhuǎn)換成字母的面試題。不過這里有一點(diǎn)值得注意的是,變量名第一個字母不能是數(shù)字,第二個字母開始可以是數(shù)字,要考慮到這一點(diǎn),才能盡可能“壓榨”變量名。
為了盡可能地“壓榨”變量名,同一級的作用域里面的變量名是可以使用相同的變量名。到下一級的時候,對子作用域進(jìn)行合并。
舉個例子:
function Mother() { var e = 'capture'; // d 不能使用跟子作用域同樣的變量名,不然子作用域無法捕獲這個變量 function A(a, b, c, d) { console.log(e); } function B(a, b, c) { // B 跟 A 函數(shù)同級,分配同樣的變量名 // ... } }
上述例子中,A 和 B 都沒有子作用域了,變量名從 0 開始分配。到給 Mother 下 e 分配變量名時,找到子作用域最大的計(jì)數(shù)器。分配最多的子作用域 A 分配了 4 個,所以 B 計(jì)數(shù)器從 5 開始分配,所以給 e 分配了5,所以 e 就得到了這個名字。
所以變量別名就是從 AST 的葉子開始向上構(gòu)造,一直分配到根結(jié)點(diǎn)把所有作用域都分配完為止。
小技巧
這里 esbuild 采用了比較聰明的技巧。它統(tǒng)計(jì)了所有變量的引用次數(shù),然后進(jìn)行排序,引用次數(shù)最多的變量分配到的名字就是盡量短的,這樣也可以減少編譯出來 js 的體積。我在寫 jetpack 打包的時候,也借鑒了這種做法。
模塊合并
模塊合并的辦法有很多。webpack 采用的是用 function 把每個函數(shù)包起來,放到了一個長長的數(shù)組里面,然后實(shí)現(xiàn)了自己的 require,esbuild 也采用了類似的方法。
Rollup.js 實(shí)現(xiàn)的方法則是作用域提升(Scope hoisting),把模塊都放到根作用域。這里我采用的方法也是作用域提升。
假設(shè)有 a.js 文件:
export function A() { console.log('a'); }
然后有 main.js 文件:
import { A as ExternalA } from './a'; function A() { console.log('local A'); } export function main() { A() + ExternalA(); }
使用 jetpack 打包完的結(jié)果:
// a.js function A() { console.log('a'); } // main.js function A_0() { console.log('local A'); } function main() { A_0() + A(); } export { main };
難點(diǎn)在于作用域合并。實(shí)際上在 ES modules 里面不同 modules 之間引用是一個圖結(jié)構(gòu)。
C++ 的優(yōu)化
除了策略上的優(yōu)化,C++ 還提供了諸多基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)/內(nèi)存方面的優(yōu)化。
shared_ptr
AST 的結(jié)點(diǎn)全部使用 shared_ptr,有人可能認(rèn)為這是一個很大的開銷。但是早期的時候我實(shí)現(xiàn)過一個裸指針版本(不釋放內(nèi)存),并沒有測出有明顯差距。
使用 shared ptr 很重要一個原因是,一個子樹可能被其他類擁有(打包模塊,Scope,ES Module 管理器)。這個時候如果用 unique ptr 的話就會 gg。只能說 GC 大法好。
對于 C++ 這種沒有 GC 的語言有一個毛病就是:析構(gòu) AST 非常耗時。AST 夠大的話能耗上十幾 ms(這個時間跟 gc 比有何優(yōu)勢?),所以因此我也能想出了一個辦法: 不釋放內(nèi)存 ……。
最后說一句: GC 大法好 。
robin hood hashing
由于打包器中大量使用哈希表,所以提高哈希表速度尤其重要,這里我使用了 robin hood hashing
參見: https://martin.ankerl.com/2019/04/01/hashmap-benchmarks-01-overview/
在 hash 方面我有一個設(shè)想,就是像 Lua 一樣,對于短字符,在字符串創(chuàng)建的時候把 hash 記下來,這樣在多次使用哈希表的時候可以節(jié)省 hash 的時間(但是要求字符串是 immutable 的)。為此我專門寫了個 String 類,最后的結(jié)果是總體速度慢了 2-3x,測出來是 immutable 字符串拼接耗時太多,最后放棄了這個方案。
jemalloc
Parsing 過程中需要大量分配 node,大家都知道很明顯 C++ 的 new 并不夠快。經(jīng)過測試在 macOS 下使用 jemalloc 會讓 parsing 速度提升 1 倍。使 用系統(tǒng) malloc 會導(dǎo)致 parsing 速度比 Go 慢 1x 左右,慢在 new 。
當(dāng)然了,內(nèi)存池我也試過的,測出來速度基本和 jemalloc 一樣,所以就直接用 jemalloc 了。
性能
總結(jié)
寫編譯器需要快速大量產(chǎn)生 node 結(jié)點(diǎn),大量樹和圖的結(jié)構(gòu),這一方面的運(yùn)算 C++ 并沒有什么優(yōu)勢可言。
不得不承認(rèn),使用 C++ 你要思考很多東西,做很多很多額外的工作,才能獲得比 Go 還快的速度(什么都不想做出來只會比 Go 還慢)。另一方面使用 C++ 會讓你額外考慮很多和業(yè)務(wù)無關(guān)的東西,大大降低開發(fā)速度,而對于打包器這個場景 C++ 在這一塊本身不能提供很大優(yōu)勢。
看完上述內(nèi)容,是不是對JavaScript打包壓縮工具的用法有進(jìn)一步的了解,如果還想學(xué)習(xí)更多內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。