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