Rust賦能前端:為WebAssembly 瘦身
1. 準(zhǔn)備工作
前端項(xiàng)目
由于我們今天的主要任務(wù)是做WebAssembly的優(yōu)化處理,前端項(xiàng)目不是我們重點(diǎn)
針對(duì)于我來說,我直接就用之前的OCR前端項(xiàng)目了。當(dāng)然,你如果不想翻看之前的文章,你也可以使用f_cli_f[1]來構(gòu)建的前端Vite+React+TS項(xiàng)目。
然后在src/pages構(gòu)建一個(gè)文件上傳的頁(yè)面,在src目錄下構(gòu)建一個(gè)wasm目錄來存放在前端項(xiàng)目中要用到的各種wasm。
Rust項(xiàng)目
我們是用之前的OCR的Rust項(xiàng)目。當(dāng)然,你也可以拿你自己的項(xiàng)目來進(jìn)行驗(yàn)證。因?yàn)?,我們此篇文章的?nèi)容,都不涉及具體的業(yè)務(wù)邏輯。
2. 前置知識(shí)點(diǎn)
之所以將后面可能涉及到的知識(shí)點(diǎn)和概念提前寫出來,是想讓行文更加明了。也是為了照顧不同階段的同學(xué)。如果你對(duì)這些概念都熟悉,那么可以直接跳過該節(jié)內(nèi)容。如果你還是Rust新手,那么這節(jié)內(nèi)容也算是一種知識(shí)的鞏固。
2.1 Rust Channel
Rust 被發(fā)布到三個(gè)不同的channel:
- stable(穩(wěn)定版):穩(wěn)定版本每 6 周發(fā)布一次
- beta(測(cè)試版):測(cè)試版是即將成為下一個(gè)穩(wěn)定版的版本
- nightly(夜間版):夜間版則是每天晚上構(gòu)建的最新版本
我們?cè)诎惭bRust后,它會(huì)安裝一個(gè)名為rustup的工具,這個(gè)工具能讓我們管理多個(gè)不同版本的 Rust。
默認(rèn)情況下,rustup會(huì)安裝stable版本到我們本機(jī)環(huán)境。
我們可以在rust 版本信息[2]中查看每個(gè)版本的各種信息。
圖片
2.2 安裝nightly版本
Nightly 版本,可以幫助我們嘗試 Rust 的最新特性,我們后面在編譯的時(shí)候,需要用到該版本
我們可以通過下面命令來安裝nightly.
rustup toolchain install nightly
系統(tǒng)將下載所需組件,包括 rustc、rust-std、cargo 等,最后安裝它們。
然后我們可以通過rustup toolchain list來查看我們本機(jī)安裝的Rust版本。
下面是我本機(jī)的各個(gè)Rust信息。
stable-x86_64-apple-darwin (default) (override)
stable-x86_64-unknown-linux-gnu
nightly-x86_64-apple-darwin
1.75.0-x86_64-apple-darwin
細(xì)心的同學(xué),可以看到在stable-x86_64-apple-darwin后面有default/override的字樣。
2.3 切換工具鏈
- 使用 rustup default 切換工具鏈
- 使用 rustup override 在特定項(xiàng)目中使用不同的工具鏈。
我們可以使用 rustup default 命令來切換到指定的工具鏈。
例如,要切換到 stable-x86_64-apple-darwin:
rustup default stable-x86_64-apple-darwin
或者如果切換到 nightly 版本:
rustup default nightly-x86_64-apple-darwin
我們可以使用以下命令,檢查 rustc 當(dāng)前使用的版本:
rustc -V
然后它就會(huì)返回當(dāng)前使用的版本信息。例如我本機(jī)的返回信息為rustc 1.81.0 (eeb90cda1 2024-09-04)
臨時(shí)使用不同工具鏈
如果我們只想在某個(gè)項(xiàng)目使用不同的工具鏈,不改變?nèi)值哪J(rèn)設(shè)置,可以使用:
rustup override set <toolchain>
例如:
rustup override set nightly-x86_64-apple-darwin
這個(gè)命令只會(huì)在當(dāng)前目錄下使用指定的工具鏈,不會(huì)影響其他項(xiàng)目或全局的默認(rèn)設(shè)置。
切換為特定版本的 Rust
我們也可以切換到已安裝的特定版本,比如 1.75.0:
rustup default 1.75.0-x86_64-apple-darwin
2.4 SIMD
在v8官網(wǎng)中有這么一篇文章 - Fast, parallel applications with WebAssembly SIMD[3]里面就介紹了很多關(guān)于SIMD的內(nèi)容,我們來將與我們相關(guān)的內(nèi)容做一下總結(jié)。
SIMD(單指令多數(shù)據(jù))指令是一類特殊指令,能夠通過同時(shí)對(duì)多個(gè)數(shù)據(jù)元素執(zhí)行相同操作來利用數(shù)據(jù)并行性。這類指令廣泛應(yīng)用于計(jì)算密集型應(yīng)用中,比如音頻/視頻編解碼器、圖像處理器等,能夠加速性能。大多數(shù)現(xiàn)代體系結(jié)構(gòu)都支持某種形式的 SIMD 指令。
我們可以從caniuse[4]中看到它的兼容性情況。
圖片
將Rust編譯為WebAssembly SIMD
在Fast, parallel applications with WebAssembly SIMD文章中,它介紹了如何將c/c++的代碼編譯為SIMD以供前端環(huán)境使用。當(dāng)然,也有將Rust編譯為SIMD的方式。其實(shí)我們比較關(guān)心這部分。
此舉有助于將 Rust 代碼高效地編譯為 WebAssembly并利用底層硬件的并行性
當(dāng)我們將 Rust 代碼編譯為目標(biāo) WebAssembly SIMD 時(shí),需要啟用simd128 LLVM 特性[5]。
我們可以直接控制 rustc 的標(biāo)志或通過環(huán)境變量 RUSTFLAGS,可以傳遞 -C target-feature=+simd128:
rustc … -C target-feature=+simd128 -o out.wasm
或者使用 Cargo:
RUSTFLAGS="-C target-feature=+simd128" cargo build
當(dāng)啟用了 simd128 特性時(shí),LLVM 的自動(dòng)矢量化器會(huì)默認(rèn)在優(yōu)化代碼時(shí)啟用。
2.5 binaryen
Binaryen[6] 是一個(gè)為 WebAssembly 設(shè)計(jì)的編譯器和工具鏈基礎(chǔ)庫(kù),由 C++ 編寫。它旨在讓編譯為 WebAssembly 變得簡(jiǎn)單、快速且高效
Binaryen為我們提供了很多優(yōu)化工具,而今天我們選擇其中的一個(gè)也就是-wasm-opt。
安裝wasm-opt
wasm-opt 是 Binaryen 工具的一部分,可以通過多種方式安裝,下面列出了幾種常用的安裝方法:
- 使用 npm 安裝:安裝完成后,wasm-opt 會(huì)成為全局命令,直接在終端中使用
npm install -g binaryen
- 通過 Homebrew 安裝(適用于 macOS 和 Linux)
brew install binaryen
- 通過預(yù)編譯二進(jìn)制文件安裝:前往 Binaryen的GitHub releases 頁(yè)面[7],下載與你的操作系統(tǒng)相匹配的壓縮包。
驗(yàn)證安裝
安裝完成后,我們可以通過以下命令檢查 wasm-opt 是否安裝成功:
wasm-opt --version
// wasm-opt version 119 (version_119)
如果返回版本號(hào),說明安裝成功。
3. 常規(guī)編譯
我們之前在Rust 編譯為 WebAssembly 在前端項(xiàng)目中使用就介紹過,如何將一個(gè)Rust項(xiàng)目編譯為WebAssembly。
當(dāng)時(shí)我們使用常規(guī)的編譯方式。
cargo build --release --target wasm32-unknown-unknown --package xxx
wasm-bindgen target/wasm32-unknown-unknown/release/xxx.wasm --out-dir yyy --target web
上面我們是通過cargo和wasm-bindgen編譯Rust文件為WebAssembly,然后在yyy的文件下生成相關(guān)的文件資源。
- xxx.wasm
- xxx.js
- xxx.d.ts
然后,我們就可以將yyy的相關(guān)文件引入到前端項(xiàng)目中,通過配置Webpack/Vite的Wasm相關(guān)內(nèi)容,就可以通過import引入對(duì)應(yīng)的實(shí)例或者方法了。
release的默認(rèn)profiles配置
在Cargo Book[8]中對(duì)release的默認(rèn)profiles配置有相關(guān)介紹。
當(dāng)我們?cè)谑褂胏argo build --release對(duì)項(xiàng)目進(jìn)行打包處理時(shí)候,它內(nèi)部默認(rèn)是根據(jù)下面的配置優(yōu)化相關(guān)項(xiàng)目的。
[profile.release]
opt-level = 3
debug = false
split-debuginfo = '...' # Platform-specific.
strip = "none"
debug-assertions = false
overflow-checks = false
lto = false
panic = 'unwind'
incremental = false
codegen-units = 16
rpath = false
針對(duì)上面各個(gè)屬性的解釋,大家可以翻看release相關(guān)解釋[9]去了解更多,這里就不在贅述了。
效果展示
下面的所有的效果展示,和自己的本機(jī)環(huán)境息息相關(guān),也就是如果你在編譯/執(zhí)行項(xiàng)目時(shí),電腦資源被占用的很多或者電腦過熱。這個(gè)時(shí)間也是有波動(dòng)的。最終的時(shí)間對(duì)比,按自己的情況而定。
資源大小
首先,我們先看編譯后的文件大小
編譯的文件大小為1.4M
編譯時(shí)間
編譯時(shí)間為40秒
運(yùn)行時(shí)間
我們將上面編譯好的文件引入到之前我們的OCR的前端項(xiàng)目。然后,運(yùn)行相關(guān)代碼。
圖片
在執(zhí)行相關(guān)的操作后,整體的運(yùn)行時(shí)間為4秒
4. 優(yōu)化編譯詳解
寫在最前面,下面的一些配置,有最大力度的優(yōu)化方案,但是可能根據(jù)項(xiàng)目性質(zhì)的不同,你使用了,卻沒達(dá)到想要的效果。這就是一個(gè)取舍問題,也是一個(gè)實(shí)踐出真知的問題。要想將自己的項(xiàng)目配置成最好,下面的配置方案可能適用你,也可能不使用。如果不適用,你可以根據(jù)下面的配置方向,找出符合你的最佳方案。
我們來針對(duì)上面的打包做一次優(yōu)化處理。我們先把相關(guān)的優(yōu)化方案列舉出來,然后最后給一個(gè)最終的解決方案。
4.1 刪除符號(hào)或調(diào)試信息
這部分,我們可以通過設(shè)置release-strip的信息來優(yōu)化編譯結(jié)果。
[profile.release]
strip = true
在 Rust 項(xiàng)目中,strip它決定了 rustc是否從生成的二進(jìn)制文件中刪除符號(hào)或調(diào)試信息。這個(gè)選項(xiàng)主要用于減小生成文件的大小,特別是在發(fā)布(release)模式下打包時(shí)。
strip 的選項(xiàng)
- "none": 不剝離任何信息(默認(rèn)設(shè)置)
- "debuginfo": 剝離調(diào)試信息,但保留符號(hào)。
- "symbols": 剝離符號(hào)信息,保留調(diào)試信息。
除了字符串值外,還可以使用布爾值進(jìn)行設(shè)置:
- strip = true 等同于 strip = "symbols"。
- strip = false 等同于 strip = "none",完全禁用剝離
場(chǎng)景說明
在 Linux 和 macOS 系統(tǒng)上,編譯生成的 .elf 文件中默認(rèn)會(huì)包含符號(hào)信息。這些符號(hào)信息通常不需要在執(zhí)行二進(jìn)制文件時(shí)使用,因此可以選擇剝離,以減小文件大小。尤其是在發(fā)布模式下,剝離符號(hào)信息是常見的做法,用來生成更小、更優(yōu)化的可執(zhí)行文件。
4.2 設(shè)置opt-level
Rust 的 opt-level 設(shè)置控制 rustc 的 -C opt-level 標(biāo)志,它用于決定編譯時(shí)的優(yōu)化級(jí)別。
優(yōu)化級(jí)別越高,生成的代碼在運(yùn)行時(shí)可能越快,但同時(shí)也會(huì)增加編譯時(shí)間,并且更高的優(yōu)化級(jí)別可能會(huì)對(duì)代碼進(jìn)行重排和改動(dòng),這可能會(huì)使調(diào)試更加困難。
可用的優(yōu)化級(jí)別
- 0: 無優(yōu)化,適合快速編譯,常用于開發(fā)階段。
- 1: 基礎(chǔ)優(yōu)化,平衡編譯速度和性能,適合某些性能需求不高的場(chǎng)景。
- 2: 中度優(yōu)化,進(jìn)行一些優(yōu)化,提供較好的性能,通常用于測(cè)試環(huán)境。
- 3: 完全優(yōu)化,進(jìn)行所有可能的優(yōu)化,適合需要最高性能的發(fā)布代碼,但編譯時(shí)間會(huì)增加。
- "s": 優(yōu)化二進(jìn)制文件大小,通過減少代碼體積來優(yōu)化。
- "z": 進(jìn)一步優(yōu)化二進(jìn)制大小,并關(guān)閉循環(huán)向量化,使得編譯產(chǎn)物更小。
這里我們選擇大力出奇跡直接使用最高級(jí)別的優(yōu)化。
[profile.release]
opt-level = "s"
其實(shí),每個(gè)項(xiàng)目的優(yōu)化力度是不同的,這個(gè)需要根據(jù)自己項(xiàng)目去決定
4.3 Link Time Optimization (LTO)
Link Time Optimization (LTO) 是一種優(yōu)化技術(shù),它將編譯單元在鏈接階段進(jìn)行優(yōu)化。通常情況下,Cargo 會(huì)將每個(gè)編譯單元獨(dú)立編譯和優(yōu)化,而 LTO 允許在整個(gè)程序的鏈接階段對(duì)其進(jìn)行優(yōu)化。這可以去除不需要的代碼(例如死代碼),并且在許多情況下會(huì)減小二進(jìn)制文件的大小。這個(gè)和我們前端的TreeSharke是一個(gè)道理。
lto 設(shè)置的選項(xiàng)
- false: 執(zhí)行“局部精簡(jiǎn) LTO”,即在本地 crate 的代碼生成單元上執(zhí)行精簡(jiǎn) LTO。如果 codegen-units 是 1 或者 opt-level 為 0,則不會(huì)進(jìn)行 LTO。
- true 或 "fat": 執(zhí)行“胖 LTO”,嘗試對(duì)整個(gè)依賴圖中的所有 crate 進(jìn)行跨 crate 優(yōu)化。
- "thin": 執(zhí)行“精簡(jiǎn) LTO”,類似于“胖 LTO”,但消耗的時(shí)間大大減少,同時(shí)仍能獲得類似的性能提升。
- "off": 禁用 LTO。
這里我們也是下猛藥。直接使用最大力度的優(yōu)化方案。
LTO 的配置方法
[profile.release]
lto = true
4.4 設(shè)置并行代碼生成單元
在 Rust 中,代碼生成單元(codegen-units) 是編譯器將 crate 拆分為多個(gè)部分并行處理的機(jī)制。通過增加代碼生成單元,編譯器可以并行處理多個(gè)部分,從而加快編譯速度。然而,更多的代碼生成單元會(huì)限制某些全局優(yōu)化的能力,從而可能導(dǎo)致較大的二進(jìn)制文件或運(yùn)行速度稍慢的代碼。
減少代碼生成單元數(shù),尤其是在發(fā)布模式下,將有助于 Rust 編譯器執(zhí)行更深入的全局優(yōu)化,生成更高效和更小的二進(jìn)制文件。在性能需求高或者文件大小敏感的場(chǎng)景下,將 codegen-units 設(shè)置為 1 是一種常見的優(yōu)化手段。
并行代碼生成單元的設(shè)置
Rust 默認(rèn)在發(fā)布構(gòu)建中將 crate 分成 16 個(gè)并行代碼生成單元。這種設(shè)置有助于加快編譯速度,特別是在多核 CPU 上,因?yàn)槎鄠€(gè)單元可以同時(shí)生成代碼。然而,這會(huì)限制編譯器進(jìn)行某些全局優(yōu)化,例如跨模塊優(yōu)化,影響代碼運(yùn)行時(shí)的性能或二進(jìn)制文件的大小。
權(quán)衡
- 更多并行單元:編譯速度更快,但可能會(huì)損失全局優(yōu)化的機(jī)會(huì)。
- 更少并行單元(如 1):編譯速度較慢,但生成的代碼經(jīng)過更多全局優(yōu)化,可能運(yùn)行速度更快,并且二進(jìn)制文件更小。
我們的選擇
我們選擇將codegen-units設(shè)置為1,犧牲編譯速度,減少文件大小。
[profile.release]
codegen-units = 1
4.5 修改panic!()行為
當(dāng) Rust 代碼執(zhí)行 panic!() 時(shí),默認(rèn)的行為是 展開棧(unwinding the stack),從而生成有用的回溯信息(backtrace),以幫助我們定位問題。然而,棧展開過程需要額外的代碼,這增加了二進(jìn)制文件的大小。
為了減少二進(jìn)制文件的大小,Rust 提供了另一種策略,即在程序出現(xiàn) panic!() 時(shí),立即 終止進(jìn)程(abort),而不是展開棧。通過啟用這種行為,可以完全去掉棧展開代碼,顯著減少程序的二進(jìn)制大小。
啟用 Abort on Panic
在 Cargo.toml 中通過在發(fā)布配置下設(shè)置 panic = "abort" 來啟用此功能:
[profile.release]
panic = "abort"
使用 "abort" 策略可以有效減少二進(jìn)制文件的大小,特別適合生產(chǎn)環(huán)境和資源受限的場(chǎng)景,但會(huì)犧牲部分調(diào)試能力和安全性,所以這就要求我們?cè)谇岸谁h(huán)境做一些容錯(cuò)機(jī)制。
4.6 移除位置信息
在 Rust 中,默認(rèn)情況下,panic!() 和 #[track_caller] 特性會(huì)生成文件、行號(hào)和列號(hào)的位置信息,用于在代碼運(yùn)行出錯(cuò)時(shí)提供更有用的回溯信息(traceback)。這些信息對(duì)調(diào)試非常有幫助,但也會(huì)增加二進(jìn)制文件的大小。
為了進(jìn)一步減小生成的二進(jìn)制文件的大小,Rust 提供了一個(gè)實(shí)驗(yàn)性的功能,可以移除這些位置信息。這通過使用 rustc 的不穩(wěn)定選項(xiàng) -Zlocation-detail 來實(shí)現(xiàn)。
有效的 location-detail 選項(xiàng):
- none:移除所有文件、行號(hào)和列號(hào)信息。適合在不需要回溯信息的環(huán)境中使用。
- file:僅保留文件信息。
- file,line:保留文件和行號(hào)信息。
- file,line,column(默認(rèn)值):保留完整的文件、行號(hào)和列號(hào)信息,用于調(diào)試。
移除位置信息
通過設(shè)置 RUSTFLAGS 環(huán)境變量并將其值設(shè)為 -Zlocation-detail=none,我們可以在構(gòu)建二進(jìn)制時(shí)移除這些位置信息,從而減少文件大小。這種優(yōu)化特別適用于生產(chǎn)環(huán)境,或者對(duì)二進(jìn)制文件大小有較高要求的項(xiàng)目。
示例命令如下:
$ RUSTFLAGS="-Zlocation-detail=none" cargo +nightly build --release
從上面可以看到,有一段cargo +nightly。這說明啥,這需要我們切換到nightly版本。
4.7 移除 fmt::Debug
在 Rust 中,#[derive(Debug)] 和 {:?} 格式化符號(hào)用于調(diào)試輸出,幫助我們打印結(jié)構(gòu)體和枚舉的內(nèi)部信息。然而,調(diào)試功能會(huì)在生成的二進(jìn)制文件中包含大量類型信息和格式化函數(shù),這可能會(huì)增加文件大小。
fmt-debug 選項(xiàng)說明:
- **full**(默認(rèn)):#[derive(Debug)] 遞歸打印類型及其字段的詳細(xì)信息。
- **shallow**:僅打印類型名稱或枚舉的變體名稱,不打印詳細(xì)的類型字段信息。此行為不穩(wěn)定,未來可能會(huì)有變化。
- **none**:完全不打印任何信息,{:?} 格式化符號(hào)也不起作用。此選項(xiàng)可以顯著減少二進(jìn)制文件大小,并移除沒有被符號(hào)剝離移除的類型名稱,但可能導(dǎo)致 panic! 和 assert! 消息不完整。
移除 fmt::Debug:
Rust 提供了一個(gè)實(shí)驗(yàn)性選項(xiàng) -Zfmt-debug,允許將 #[derive(Debug)] 和 {:?} 格式化操作變成空操作(no-op),即不輸出任何調(diào)試信息。通過這種方式,派生的 Debug 實(shí)現(xiàn)和相關(guān)的字符串將被移除,從而減小二進(jìn)制文件的大小。
可以使用如下命令啟用該功能:
$ RUSTFLAGS="-Zfmt-debug=none" cargo +nightly build --release
和之前的location-detail一樣,開啟該項(xiàng)目功能,我們也需要使用Rust的nightly版本。
4.8 進(jìn)一步優(yōu)化 panic
panic_immediate_abort,旨在徹底移除 panic!() 相關(guān)的字符串格式化邏輯。這是 panic = "abort" 選項(xiàng)的進(jìn)一步優(yōu)化,即便已指定了 panic = "abort",Rust 仍然會(huì)默認(rèn)將一些與 panic!() 相關(guān)的字符串和格式化代碼包含在最終的二進(jìn)制文件中。這會(huì)導(dǎo)致二進(jìn)制文件中存在不必要的占用空間,尤其是在極致優(yōu)化二進(jìn)制大小的場(chǎng)景下。
如何使用
配置方式如下:
$ cargo +nightly build \
-Z build-std=std,panic_abort \
-Z build-std-features=panic_immediate_abort
- **使用 build-std 重新構(gòu)建 libstd**:按照 build-std 的流程,重新編譯標(biāo)準(zhǔn)庫(kù),同時(shí)啟用 panic_abort 行為。
- 進(jìn)一步縮小二進(jìn)制大?。?jiǎn)⒂?panic_immediate_abort 特性后,所有與 panic!() 相關(guān)的字符串信息和格式化邏輯都將被移除。
4.9 開啟simd128
之前我們就說過,我們可以對(duì)Rust開啟simd128。
RUSTFLAGS="-C target-feature=+simd128" cargo build
這里就不在過多解釋了。
4.10 優(yōu)化wasm-bindgen
之前的優(yōu)化都是針對(duì)Rust部分,下面我們來看看,針對(duì)wasm-bindgen的優(yōu)化角度。
之前我們不是,使用wasm-bindgen為 Rust 編寫的 WebAssembly 模塊生成 JavaScript 綁定。它可以幫助 Rust 和 JavaScript 之間進(jìn)行高效的數(shù)據(jù)交互。
wasm-bindgen target/wasm32-unknown-unknown/release/audioAndVideo.wasm --out-dir js/dist/ --target web
我們還可以在后面添加一下配置,來優(yōu)化生成的代碼。
--reference-types
- 此選項(xiàng)啟用了 WebAssembly 的引用類型,這允許 WebAssembly 代碼可以直接引用 JavaScript 對(duì)象(如 DOM 元素),無需對(duì)這些對(duì)象進(jìn)行包裝或轉(zhuǎn)換,從而提高了內(nèi)存管理和交互的效率。
- 引用類型擴(kuò)展了 WebAssembly 中的基本值類型(如 i32, i64, f32, f64),引入了可以引用 JavaScript 對(duì)象(如函數(shù)、外部引用)的能力。
--reference-types 通過允許 WebAssembly 直接引用 JavaScript 對(duì)象,提高了效率,減少了不必要的轉(zhuǎn)換。
--weak-refs
- 此選項(xiàng)啟用了 WebAssembly 中的弱引用。弱引用允許我們引用對(duì)象而不會(huì)阻止它們被垃圾回收。在 JavaScript 中,這一特性被用于防止內(nèi)存泄漏,可以持有對(duì)象的弱引用,當(dāng)沒有強(qiáng)引用時(shí),垃圾回收器可以回收這些對(duì)象。
- 對(duì)于 Rust 和 WebAssembly,弱引用有助于在跨 JS 和 WebAssembly 邊界的對(duì)象跟蹤中進(jìn)行更有效的內(nèi)存管理,避免不必要的對(duì)象占用內(nèi)存。
--weak-refs 通過啟用弱引用,改善了內(nèi)存管理,防止內(nèi)存泄漏,確保不必要的對(duì)象能被及時(shí)回收。
4.11 使用wasm-opt
由于,我們?cè)谇懊嬉呀?jīng)下載了wasm-opt了。所以,這里我們就直接上代碼了。
我們?cè)赗ust項(xiàng)目中構(gòu)建一個(gè)tools/optimize-wasm.sh文件。
內(nèi)容如下:
#!/bin/sh
set -eu
BIN_PATH="${1:-}"
WASMOPT_BIN=$(which wasm-opt || true)
if [ -z "$BIN_PATH" ]; then
echo "Usage: $(basename "$0") <WASM binary>"
exit 1
fi
if [ -z "$WASMOPT_BIN" ]; then
echo '由于未找到 `wasm-opt` 二進(jìn)制文件,因此跳過編譯后優(yōu)化。'
exit
fi
if [ -n "${SKIP_WASM_OPT:-}" ]; then
echo "由于設(shè)置了 SKIP_WASM_OPT,所以跳過了編譯后優(yōu)化"
exit
fi
wasm-opt --enable-simd --enable-reference-types -O2 "$BIN_PATH" -o "$BIN_PATH".optimized
mv "$BIN_PATH.optimized" "$BIN_PATH"
上面代碼,最關(guān)鍵的就是
wasm-opt --enable-simd --enable-reference-types -O2 "$BIN_PATH" -o "$BIN_PATH".optimized
mv "$BIN_PATH.optimized" "$BIN_PATH"
- 使用以下選項(xiàng)運(yùn)行 wasm-opt 對(duì)提供的 WASM 二進(jìn)制文件進(jìn)行優(yōu)化:
--enable-simd: 啟用 SIMD 支持,以便更快地進(jìn)行并行數(shù)據(jù)處理。
--enable-reference-types: 啟用 WASM 模塊中的引用類型。
-O2: 應(yīng)用中等級(jí)別的優(yōu)化 (O2),在性能和二進(jìn)制大小之間進(jìn)行平衡。
優(yōu)化結(jié)果寫入臨時(shí)文件 "$BIN_PATH.optimized"。
- 最后,通過移動(dòng) (mv) 優(yōu)化后的文件替換原始文件。
5. 最終方案
配置Cargo.toml
[profile.release]
strip = true
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
構(gòu)建shell 文件
注意下文中的xxx需要替換成你項(xiàng)目的名稱
build.sh
我們?cè)陧?xiàng)目根目錄構(gòu)建一個(gè)build.sh文件,內(nèi)容如下
#!/bin/bash
# 執(zhí)行 optimize-rust.sh
echo "執(zhí)行 optimize-rust.sh..."
./tools/optimize-rust.sh
# 檢查是否成功執(zhí)行
if [ $? -ne 0 ]; then
echo "optimize-rust.sh 構(gòu)建失敗."
exit 1
fi
# 執(zhí)行 optimize-wasm.sh
echo "執(zhí)行 optimize-wasm.sh..."
./tools/optimize-wasm.sh js/dist/xxx_bg.wasm
# 檢查是否成功執(zhí)行
if [ $? -ne 0 ]; then
echo "optimize-wasm.sh 構(gòu)建失敗."
exit 1
fi
echo "Rust 已構(gòu)建成功,到指定目錄查看相關(guān)信息."
我們構(gòu)建一個(gè)tools文件,然后新建兩個(gè)文件
- optimize-rust.sh
- optimize-wasm.sh
optimize-rust.sh
#!/bin/bash
# 獲取項(xiàng)目名稱
PACKAGE_NAME="xxx"
# 編譯 Rust 代碼
RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none -C target-feature=+simd128" cargo +nightly build \
-Z build-std=std,panic_abort \
-Z build-std-features=panic_immediate_abort \
--release --target wasm32-unknown-unknown --package "$PACKAGE_NAME"
# 生成 wasm 綁定
wasm-bindgen target/wasm32-unknown-unknown/release/"$PACKAGE_NAME".wasm --out-dir js/dist/ --target web
optimize-wasm.sh
#!/bin/sh
set -eu
BIN_PATH="${1:-}"
WASMOPT_BIN=$(which wasm-opt || true)
if [ -z "$BIN_PATH" ]; then
echo "Usage: $(basename "$0") <WASM binary>"
exit 1
fi
if [ -z "$WASMOPT_BIN" ]; then
echo '由于未找到 `wasm-opt` 二進(jìn)制文件,因此跳過編譯后優(yōu)化。'
exit
fi
if [ -n "${SKIP_WASM_OPT:-}" ]; then
echo "由于設(shè)置了 SKIP_WASM_OPT,所以跳過了編譯后優(yōu)化"
exit
fi
wasm-opt --enable-simd --enable-reference-types -O2 "$BIN_PATH" -o "$BIN_PATH".optimized
mv "$BIN_PATH.optimized" "$BIN_PATH"
然后,我們就可以在Rust項(xiàng)目根目錄執(zhí)行./build.sh來執(zhí)行編譯任務(wù)了。
運(yùn)行結(jié)果
文件大小
編譯的文件大小為900KB
可以看到,我們將之前1.4MB的資源縮小到了900KB。
如果我們還想減少二進(jìn)制文件的大小,我們還可以繼續(xù)更改上面的配置信息。如果單純的追求資源大小的話,我們可以將其縮小到300kb,但是,其運(yùn)行時(shí)間會(huì)比沒瘦身之前還長(zhǎng)
魚和熊掌不能兼得,我們只能根據(jù)實(shí)際情況而定。也就是實(shí)踐出真知
運(yùn)行時(shí)間
圖片
可以看到,雖然文件大小變小了,但是我們運(yùn)行性能卻沒有打折扣。那就充分說明,我們此次的瘦身是成功的。
Reference
[1]f_cli_f: https://www.npmjs.com/package/f_cli_f
[2]rust 版本信息: https://releases.rs/
[3]Fast, parallel applications with WebAssembly SIMD: https://v8.dev/features/simd
[4]caniuse: https://caniuse.com/?search=simd
[5]LLVM 特性: https://llvm.org/
[6]Binaryen: https://github.com/WebAssembly/binaryen
[7]Binaryen的GitHub releases 頁(yè)面: https://github.com/WebAssembly/binaryen/releases
[8]Cargo Book: https://doc.rust-lang.org/cargo
[9]release相關(guān)解釋: https://doc.rust-lang.org/cargo/reference/profiles.html