如何生成最小体积 .wasm?

解读

面试官问“最小体积”并不是想听一句“cargo build --release”就结束,而是考察候选人是否真正踩过“把几兆 Rust 代码压到几十 KB”的线上坑。国内 WebAssembly 落地场景集中在小程序引擎、浏览器渲染模块、区块链合约沙箱、直播滤镜 SDK,体积直接决定 CDN 费用和首帧时间,因此招聘方希望听到可量化的优化路径、工具链熟练度、对链接器行为的理解,以及对业务取舍的敏感度

知识点

  1. 编译配置三板斧:codegen-units=1、lto=true、panic=abort
  2. 尺寸剖析工具:twiggy、wasm-opt、wasm-strip、wasm-bindgen-cli 的 --reference-types 与 --weak-refs 开关
  3. 语言层面取舍:禁用 std(#![no_std])、关闭格式化/浮点、用 wee_alloc 或 lol_alloc 替代默认 jemalloc、避免泛型爆炸
  4. 链接器级优化:-C link-arg=-zstack-size=16384、-C link-arg=--gc-sections、-C link-arg=--strip-all、llvm 的 internalize 与 globaldce
  5. wasm-opt 的 -Oz 与 -Os 差异,以及国内网络环境常用的阿里 OSS/腾讯云函数对 100 KB 以下冷启动免计费的潜规则
  6. Cargo profile 分层:dev、release 之外再定义一个 wasm-release,避免把调试符号带到生产
  7. wasm-bindgen 的 --split-linked-modules 与 webpack 的 async wasm 加载结合,实现首包只加载 20 KB 的“骨架”

答案

第一步,在 Cargo.toml 中新建专用 profile

[profile.wasm-release]
inherits = "release"
opt-level = "z"          # 体积优先
lto = true               # 全量链接时优化
codegen-units = 1        # 单 codegen-unit,让 LLVM 看到整个调用图
panic = "abort"          # 去掉 unwind 表

第二步,编译时显式关闭标准库与调试符号

RUSTFLAGS="-C link-arg=-zstack-size=16384 -C link-arg=--gc-sections -C link-arg=--strip-all" \
cargo build --target wasm32-unknown-unknown --profile wasm-release -Z build-std=core,alloc -Z build-std-features=panic_immediate_abort

第三步,用 Binaryen 工具链二次压缩

wasm-strip target/wasm32-unknown-unknown/wasm-release/xxx.wasm
wasm-opt -Oz --enable-bulk-memory --enable-sign-ext -o xxx.min.wasm xxx.wasm

第四步,验证尺寸与符号

twiggy top -n 10 xxx.min.wasm
ls -lh xxx.min.wasm

按以上四步,一个空壳 Rust 项目可从 750 KB 压到 28 KB;若再配合 wee_alloc 与手动 #![no_std],常见算法库可稳定落在 15 KB 以内,满足国内小程序<50 KB 的上线红线。

拓展思考

  1. 如果业务必须保留格式化与浮点,能否用**“动态链接 + side module”**把浮点解析放到宿主 JS,从而让主 .wasm 恒小于 30 KB?
  2. 当 wasm-opt -Oz 导致冷启动耗时增加 15% 时,如何在体积-性能-冷启动三角里给老板一个可量化的决策公式?
  3. 国内 CDN 普遍支持 Brotli-11,把 wasm-opt 后的二进制再交给 Brotli 可再省 8%–12%,但边缘节点回源耗时上涨,如何设计 A/B 实验验证 ROI?
  4. 未来 Rust 1.80 的 “-Zbuild-std=std,panic_abort” 将允许在 no_std 与 std 之间做“部分标准库”,届时是否还需要手写 #![no_std]?