作者 |?馬可 ? ? ?
小程序編譯器是百度開發(fā)者工具中的編譯構建模塊,用來將小程序代碼轉換成運行時代碼。舊版編譯器由于業(yè)務發(fā)展,存在編譯慢、內存占用高的問題,我們對編譯器做了一次大規(guī)模的重構,采用自研架構,做了多線程、代碼緩存、sourcemap 等多項優(yōu)化,在性能和內存占用上都有很大提升。全文介紹了新版編譯器的設計思路和優(yōu)化方法,以及一些能夠用在通用打包工具里的技術點。? ?
01
前言
小程序編譯器在小程序開發(fā)、預覽、發(fā)布各個階段都需要使用,因此編譯器性能會直接影響到開發(fā)者開發(fā)效率,也會影響到開發(fā)者工具的使用體驗。 由于舊版的編譯器(基于 webpack4)在構建大型項目時會很慢,內存占用也高,一直被開發(fā)者吐槽。我們經過大量的調研和開發(fā),最后采用完全自研架構做新編譯,針對小程序項目構建做了大量優(yōu)化,基本解決了舊編譯存在的問題。 下圖是部分項目構建時間對比:
新版編譯器相對于舊版實現了 2~7 倍的性能提升,并且支持實時編譯、熱重載等特性,內存占用更少,構建產物更優(yōu)。
下面從 框架選型、新編譯器工作原理、性能和產物優(yōu)化方法 等方面介紹新版編譯器的成長之路。
GEEK TALK
02
框架選型
在進行新版編譯器設計時,需要明確當前的痛點問題:性能,優(yōu)先解決性能問題。其他新技術和新想法對編譯器有幫助的也一起實施。
舊版編譯器基于 webpack4 存在如下幾個問題:
大型項目構建速度太慢。
dev 啟動慢、增量編譯慢,僅支持 loader 緩存,bundle 無緩存也比較慢。
基于 webpack4 做擴展開發(fā),需要 patch 部分模塊才能工作,維護困難。
部分 webpack bundle 過程無法針對小程序代碼結構進行優(yōu)化,存在無效構建。
新編譯的設計目標:
更快的全量編譯速度,消除 webpack 存在的無效構建過程。
支持全緩存,加快首次和增量編譯速度。
支持實時編譯,減少 dev 啟動和二次編譯時間。
支持多線程編譯加速,支持頁面熱重載。
優(yōu)化產物結構,減少產物體積。
2.1 主流構建工具
下面介紹的是我們調研過的主流前端構建工具,每個工具都有適用場景和優(yōu)缺點。
在新版本編譯器架構設計時,其他構建工具的設計理念和技術特點都值得參考。
Webpack 構建過程:
Webpack 優(yōu)點:功能完善、社區(qū)活躍、可配置性強、有很強的擴展性。
Webpack 缺點:配置復雜、構建速度慢,二次開發(fā)困難。
Parcel 構建過程:
Parcel 優(yōu)點:無需配置,構建速度快,原生支持多線程和全緩存,多線程之間共享數據通過 lmdb 進行,避免跨線程通信開銷。
Parcel 缺點:生態(tài)小,自定義性有限,大量采用 Node 插件,兼容性也差一些。
Vite 構建過程:
Vite 優(yōu)點:配置較為簡單,按需編譯,啟動快,dev 時有不錯的體驗。
Vite 缺點:生態(tài)小,dev 和 發(fā)布走兩套構建流程。
其他小程序平臺:
微信基于 gulp 和 C++?模塊做小程序構建,并且對 npm 模塊做了預構建,在性能和開發(fā)體驗上做的比較好。
支付寶基于 webpack 做小程序構建,并且使用了 esbuild 加速代碼壓縮。
抖音小程序使用自研編譯器,構建流程比較簡單。
2.2 新版編譯器
在設計新編譯框架時,借鑒了主流打包工具的工作流程,結合小程序代碼特點,決定不做通用打包工具,重點優(yōu)化小程序打包性能。
最終選擇了自研編譯器的方案,并做了大量優(yōu)化工作,新版編譯器優(yōu)化點有如下幾個方面:
1.支持多 Compiler 協同工作,將動態(tài)庫開發(fā)等多類型項目構建解耦。
2.編譯階段全流程緩存,節(jié)省二次構建時間 90% 以上。
3.dev 開發(fā)默認采用按需編譯,提升單頁編譯性能。
4.支持 babel 和 swc 多線程編譯,提升全量編譯速度 2 ~ 7 倍。
5.采用新版 sourcemap 協議,移除非必要解析合并,將 bundle 階段耗時大幅縮減。
6.對 js、css、swan 模板編譯均做了構建時標記優(yōu)化,減少 bundle 合并耗時。
7.對于預覽、發(fā)布階段的 js 壓縮和混淆,采用了 terser 和 esbuild 并行方案,esbuild 用于快速打出預覽包,terser 可以保證壓縮率用于發(fā)布包。
從結果看,新編譯器從速度、資源占用和可維護性上相對于舊版都有顯著的提升。
GEEK TALK
03
新版編譯器工作原理
新編譯器的處理流程和 parcel 比較類似,Compiler 控制處理流程,Processor 進行代碼轉換,基本流程如下:
其中幾個重要的模塊:
CompileEntry 編譯器為入口模塊,包含 cli 通信、dev server 通信、命令調用等。
CompileManager 為編譯管理器,用于依賴資源下載和管理以及多個 Compiler 協同構建。
Compiler 為編譯器模塊,用于將項目源碼編譯成運行時代碼,項目構建時 Compiler 可能有多個。
Processor 為單元處理器,用于處理 代碼轉換、代碼合并 等單個編譯任務。
注:小程序 App 項目有 1 個Compiler,動態(tài)庫和動態(tài)擴展項目 2 個Compiler。
3.1 Compiler 編譯器
用于編譯單個小程序項目,將開發(fā)者原始代碼編譯為可運行代碼。
工作職能:
1.創(chuàng)建運行上下文,提供 config、fs 文件處理、watcher 監(jiān)控、logger 等模塊,給 Processor 使用。
2.全量編譯、文件變更時二次編譯;這里二次編譯也是走一遍全量編譯流程,不過大部分用的是緩存結果。
3.管理、調度、運行 Processor 處理單元。
4.維護 Processor 依賴關系和結果緩存。
特點:
1.實現全流程緩存,將每個 Processor 的輸入參數、輸出結果寫入緩存,在有緩存情況下二次編譯時長可減少 90% 。
2.支持按需編譯,每次按需單頁編譯、增量編譯、全量編譯 都走同樣的 Processor 處理流程。
3.通過 Proxy 機制自動計算緩存參數依賴,不用手動為每個 Processor 生成緩存 hash,相對于 webpack 或 parcel 減少 bug 產生。
4.僅維護 Processor 依賴關系,不維護 ModuleGraph,簡化處理流程。
關于全流程緩存每家打包器都有自己的實現方案,基本原理是根據當前輸入參數和依賴情況為處理單元生成一個唯一 hash,hash 一致則結果一致。
webpack 和 parcel 由于維護了 ModuleGraph,緩存的計算和重用會復雜一些。小程序編譯器僅根據 Processor 入參和調用依賴進行計算。
3.2?Processor 單元處理器
Processor 有如下特性:
1.在輸入參數一致的情況下,保證輸出一致,輸入和輸出都必須可序列化為 json ,實現了 Processor 全緩存。
2.Processor 中的 uri 為構建 ID,在單次構建過程中 ID 一致則處理結果一致,例如處理 app.js 文件,uri 為:js:app.js,好處是可以統(tǒng)一 Processor 資源處理路徑。
3.Processor 之間支持互相調用:processWith 調用并繼續(xù)執(zhí)行,processWithResult 調用并等待返回結果。
注意:這里的輸入參數包含 uri、app config, contextFreeData。
幾種常用的 Processor:
1.JS Processor 將 es6 代碼轉換成 es5 代碼,這是最耗時的模塊。
2.Swan Processor 將 swan 模板代碼轉換成 view 層 js 代碼。
3.Css Processor 使用 postcss 處理 css 中的單位轉換、依賴收集等工作。
4.Bundle Processor 將前面 transformer 處理結果按照 bundle 算法合并文件并輸出結果。
Processor 工作流程:
Processor 處理流程需要經過 transform -> bundle 的過程,在小程序里 js, css, swan 模板的 bundle 可以分開并行處理,這里和 webpack 的處理模式不一樣,和 parcel 的 pipeline 類似。
3.3?性能和產物優(yōu)化方法
3.3.1 多核心編譯優(yōu)化
由于 Node 中多線程模塊初始化速度和通信效率比多進程好一些,新編譯選擇使用 多線程 做多核心優(yōu)化。
多線程編譯有 2 種方案選擇:
方案1:基于 processor 做多線程調度,由于 processor 間支持相互調用,實際處理會很復雜且有通信成本。
舊的編譯器做過基于webpack 的 workerthread-loader,性能提升有限(10%~15%)。
parcel 基于 lmdb 公共緩存消除線程間通信,保證讀寫效率,是一個比較好的解決方法。
方案2:僅對 js 轉譯做多線程調度,僅有一來一回 2 次通信成本。
使用 jest-worker 和 babel transform 做 js 多線程轉譯或者用 swc 多線程做 js 轉譯。
由于大部分構建時間在 js 轉譯這里(js 中有大量 node_modules 依賴,均需要轉換),css 和 swan 模塊轉換耗時少。
最終選擇方案2 僅做 js 多線程轉譯,處理流程簡單且收益較好,整體提升如下:
使用 jest-worker 多線程 babel 轉譯,4 線程可提升 1 倍以上速度。
使用 swc 做 js 轉譯,4 線程提升 4 倍以上速度。
JS Processor 多線程處理:
其中:
uri:?為處理器構建 ID
contextFreeData:?單次構建中不可變數據,例如 app.json 中的配置項
context args:全局參數,例如優(yōu)化實驗開關、多線程開關等
在 js 轉換處理時規(guī)定了 transformer 統(tǒng)一轉換接口,基于接口實現了 babel 單線程、babel 多線程、swc 轉換 3 種處理器,并且可隨時做處理器切換。
對于不同的編譯環(huán)境可以做到靈活設置:
1.開發(fā)者工具中開發(fā)者根據機器配置情況可以切換 多線程、swc 編譯模式,提升效率。
2.云編譯流水線默認開多線程編譯提高性能。
3.webIDE 默認開單線程降低資源消耗。
3.3.2 SWC 編譯優(yōu)化
新編譯器多線程模式相對于舊編譯提升了 1 倍左右,在 dev 開發(fā)時一些大型項目頁面首次編譯還是有些慢,需要10秒以上,主要耗時在 js transform 這里。
swc 目前在 js 轉譯上基本成熟了,且大部分場景能提升 4 倍以上轉譯速度,因此增加了 swc 多線程轉譯支持,將大型項目頁面首次編譯控制在了 5 秒以內。
需要編寫 2 個 swc 插件來適配 swc 轉譯:
@swanide/swc-require-rename 將 require/import/export 中的模塊提取路徑信息,以便于后續(xù)在 js 中分析模塊依賴關系。
@swanide/swc-web-debug 對 js 代碼進行插樁處理,用來支持真機調試中的斷點調試。
swc 編譯帶來的性能提升是巨大的,在使用中也發(fā)現了一些問題:
1.swc 存在內存泄露,在 dev 階段如果全量編譯次數過多,會導致內存占用很高,需手動重啟編譯器。
2.swc 插件支持的 api 較少,一部分 babel 容易實現的功能,在 swc 中很難處理。
3.swc 由于使用 rust 編寫插件,插件在不同 @swc/core 版本間不能通用,需要為不同平臺生成 swc 插件,在部署上會麻煩一些。
在實際使用中,對于一部分 swc 不能很好處理的場景,會降級到 babel 處理。
3.3.3 代碼壓縮和運行時緩存
在 dev 階段,編譯后的代碼是沒有經過壓縮的,可以在模擬器中運行。在預覽發(fā)布階段由于限制了包體積,需要做代碼壓縮以減少產物體積。
可選的代碼壓縮工具有如下 3 個:
1.terser 壓縮率高,產物體積小,速度最慢。
2.swc 壓縮快,mangle 支持不完善,壓縮率較差。
3.esbuild 壓縮最快(比 terser 快了 10 倍以上),支持 mangle,代碼壓縮率不如 terser。
最后經過對比考慮,選擇了如下壓縮方案:
1.預覽階段由于不需要 sourcemap,移除 sourcemap,并使用 esbuild 做代碼壓縮,提高預覽速度(對于自動預覽場景有很大提升)。
2.發(fā)布階段使用 terser 做多線程壓縮,并保留 sourcemap。
運行時緩存 指的是構建過程的中間結果都在內存中做了緩存,包括 Processor 處理結果 和 代碼壓縮結果,在二次構建時可以節(jié)省大部分重新構建時間。由于緩存中保留的是字符串和 json 對象,相對于基于 webpack 的舊版編譯器有 40% ~ 60% 的內存節(jié)省,在內存占用上處于可接受范圍。
3.3.4 Swan 模板處理優(yōu)化
舊的 swan 模板處理使用 swan-loader 進行模板轉換,由于設計時沒有處理好模板 import 作用域,導致 標簽以及 filter 過濾器函數只能內聯到頁面代碼中,如果模板中大量使用了 template 和 filter,最終生成的代碼體積會非常大。
新編編譯器糾正了 import 作用域關系,將編譯產物中的 template 、 filter 生成模式由內聯改為 require 引用,然后在 bundle 階段做代碼合并,使相同模塊能夠得到重用,算是填了一個大坑。
新編譯器 swan 模板處理流程:
單個 swan 文件經過 Processor 處理后可能的產物有:
component 組件模塊,用于生成頁面和自定義組件
template 模塊
filter 過濾器函數、sjs 過濾器函數
transformed document 中間代碼
將 swan 模板轉換成不同類型的 js module,并維護依賴關系,便于后續(xù)的代碼合并時更精細化的控制。
由于歷史原因 import/include 中包含 sjs 或者 template 引用時不能直接生成 template 模塊,需要在最后入口模板中生成。新編譯也提供了 template靜態(tài)編譯選項,將嚴格限制 import 作用域,可直接生成 template 模塊代碼,對于 taro 生成的小程序項目可以節(jié)約 30% 左右的產物大小。
3.3.5 Sourcemap 優(yōu)化
由于編譯器需要支持 js 代碼調試以及運行時 error 跟蹤,在 dev 和發(fā)布階段都需要生成 sourcemap。
在 webpack 中生成代碼時需要對 sourcemap 進行合并計算,較大的項目 sourcemap 合并會占用很長時間,并且每次重新編譯都要重新計算 sourcemap。
調研時發(fā)現瀏覽器 devtools 對? sourcemap 協議?的 index map 支持非常好, 新編譯器基于 index map 協議做了 sourcemap 合并優(yōu)化,由之前的多文件 sourcemap 合并計算,變成了計算生成 offset map 并拼接內容,這樣 js bundle 耗時就由原來的 幾秒到幾十秒變?yōu)榱斯潭?3 秒以內。?
一個有意思的事情是 vscode 的 js-debugger 直到 22 年 6 月份才支持 index map 調試(index map 2011 年發(fā)布的),微軟的動作稍微慢了一些。
3.3.6 后續(xù)工作
在新編譯器開發(fā)完成之后的推廣中,采用了漸進式推廣方式:
第一階段,開發(fā)者工具新舊編譯器共存,dev、預覽使用新編譯器,發(fā)布使用舊編譯器。
第二階段,內部 pipeline 預覽和發(fā)布全量使用新編譯。
第三階段,開發(fā)者工具全部切換到新編譯器。
新版編譯實際上線后還存在一些小的兼容性問題,需要盡量提前暴露問題才能做發(fā)布全量替換。
針對小程序項目,新編譯做了大量的優(yōu)化工作,部分優(yōu)化工作還沒有完成開發(fā),包括:
hmr 熱重載:開發(fā)中,由于 運行時框架、開發(fā)者工具均需要做接口適配,需要較長時間調試才能達到預期。
tree-shaking 代碼消除:對于 es6 模塊在 transform 階段可以做 tree-shaking 消減代碼。
scope-hoisting?作用域提升:理論可行,需要驗證代碼縮減效果。
新版編譯器由于需要完全兼容舊版編譯器構建結果,在 bundle 打包場景還存在優(yōu)化空間,我們在后續(xù)工作中配合運行時框架可以做更多打包產物優(yōu)化。
GEEK TALK
04
總結
新版編譯器采用自研打包方案,對比基于 webpack 的舊編譯器實現了巨大的性能提升,徹底解決了編譯慢、資源占用高的問題,相對友商的編譯器也有不錯的性能優(yōu)勢。
一些新編譯引入的優(yōu)化手段如 swc 轉譯、esbuild 壓縮、sourcemap 優(yōu)化 也能用在其他前端項目構建中,并起到加速效果。
在新編譯器項目中每個同學都非常努力,貢獻了很多奇妙的點子,遇到的大部分難題都有效解決了。我們會繼續(xù)堅持性能和產物優(yōu)化這兩個方向,不斷提升開發(fā)者體驗和運行時效率。
編輯:黃飛
? 相關推薦
如何編寫有利于編譯器優(yōu)化的代碼
1265
如何編寫有利于編譯器優(yōu)化的代碼
325
Keil修改ARM編譯器及配置方法
1723
編譯器優(yōu)化后DSP的運行速度完全沒有變化
編譯器優(yōu)化導致USART波特率配置錯誤,請問這是為什么?如何解決?
編譯器優(yōu)化打破了程序
編譯器優(yōu)化的靜態(tài)調度介紹
編譯器優(yōu)化級別
編譯器將使用最大代碼空間來獲得最大速度優(yōu)化嗎?
ARM編譯器優(yōu)化版本1.0
ARM編譯器的分類(上)
ARM編譯器錯誤和警告參考指南
Arm編譯器6.6版armclang參考指南
Keil編譯器優(yōu)化問題
S32DS C編譯器/標準S32DS C++編譯器-優(yōu)化,,(-O3) 和 (-Os) 的MCU功能和性能是否完全相同?
gcc編譯器編譯過程介紹
stm32編譯器優(yōu)化
為什么XC32編譯器優(yōu)化會產生錯誤?
使用新版本IAR編譯老版本的STM32工程
基于pCTL的循環(huán)優(yōu)化測試用例自動生成方法
如何編寫有利于編譯器優(yōu)化的代碼
如何編寫有利于編譯器優(yōu)化的代碼
請問如何在KeilμVision5上執(zhí)行ARM編譯器的代碼優(yōu)化?
cx51編譯器用戶手冊
32
SIMD計算機的優(yōu)化編譯器設計
30
Cx51編譯器使用手冊
32
IccAVR C 編譯器的使用
172
MCS-51程序空間擴展原理及編譯器優(yōu)化
100
Keil C編譯器編程規(guī)則和代碼優(yōu)化
315
基于CoSy的編譯器開發(fā)的研究
0
C編譯器及其優(yōu)化
2
編譯器跟編輯器有什么區(qū)別
28651
編譯器是如何工作的_編譯器的工作過程詳解
15011
verilog編譯指令_verilog編譯器指示語句(數字IC)
13585
TMS320C54x匯編語言工具C/C++編譯器的功能優(yōu)化詳細概述
10
MSP430優(yōu)化C/C++編譯器V 3.2用戶指南
9
MSP430優(yōu)化C/C++編譯器V 3.3用戶指南
7
MPLAB? XC8 C編譯器的架構特性
5379
如何使用編譯器進行定位優(yōu)化信息
2389
如何使用英特爾編譯器優(yōu)化Fortran、C和C ++
2866
如何解決proteus的c編譯器問題的方法
26
編譯器原理到底是怎樣的帶你簡單的了解編譯器原理
10638
華為方舟編譯器使用指南
1
使用ARM編譯器V6.15優(yōu)化以及注意事項
2540
解答編譯器是怎樣運行的
2533
基于C++編譯器的節(jié)點融合優(yōu)化方法
19
SDCC編譯器和FreeRTOS在C8051F上的開發(fā)的應用
4
基于GCC實現支持MISRAC的安全編譯器
9
Verilog HDL 編譯器指令說明
2953
如何編寫有利于編譯器優(yōu)化的代碼
1121
【GCC編譯優(yōu)化系列】實戰(zhàn)分析C代碼遇到的編譯問題及解決思路
919
交叉編譯器安裝教程
2468
編譯器如何對代碼進行優(yōu)化(上)
596
編譯器如何對代碼進行優(yōu)化(下)
599
深入淺出編譯優(yōu)化選項(上)
1371
深入淺出編譯優(yōu)化選項(下)
731
深度學習編譯器之Layerout Transform優(yōu)化
389
編譯器優(yōu)化那些事兒之區(qū)域分析
381
SDCC-Linux下的51 MCU編譯器
3209
編譯器的優(yōu)化選項
346
TVM編譯器的整體架構和基本方法
616
Android編譯優(yōu)化之混淆配置
337
評論