Minimizing WASM binaries
- 3 minutes read - 463 wordsI’ve spent time recently playing around with WebAssembly (WASM) and waPC. Rust and WASM were born at Mozilla and there’s a natural affinity with writing WASM binaries in Rust. In the WASM examples I’ve been using for WASM Transparency, waPC and MsgPack and waPC and Protobufs.
I’ve created 3 WASM binaries: complex.wasm, simplex.wasm and fabcar.wasm and each is about 2.5MB when:
cargo build --target=wasm32-unknown-unknown --release
The Rust and WebAssembly book has an excellent section titled Shrinking .wasm. Code Size. So, let’s see what help that provides.
For complex.wasm, the starting file size using the above command is: 2,473,898.
Adding Link-Time Optimizations:
[profile.release]
lto = true
Reduces the file size to: 1,112,965 (~50%)
Then, aggressively optimizing for size:
[profile.release]
lto = true
opt-level = 'z'
Reduces the file size to: 1,053,485 (~43% of original)
Let’s use twiggy to see what’s remaining:
twiggy top -n 10 ./target/wasm32-unknown-unknown/release/complex.wasm
Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼────────────────────────────────────────────────────────
296065 ┊ 28.10% ┊ "function names" subsection
85876 ┊ 8.15% ┊ custom section '.debug_str'
75273 ┊ 7.15% ┊ data[0]
56076 ┊ 5.32% ┊ custom section '.debug_info'
47403 ┊ 4.50% ┊ custom section '.debug_line'
25480 ┊ 2.42% ┊ custom section '.debug_ranges'
25067 ┊ 2.38% ┊ custom section '.debug_pubnames'
5859 ┊ 0.56% ┊ dlmalloc::dlmalloc::Dlmalloc::malloc::h86eb72a884c31be6
5829 ┊ 0.55% ┊ <protobuf::descriptor::DescriptorProto
4678 ┊ 0.44% ┊ <protobuf::descriptor::FileDescriptorProto
425879 ┊ 40.43% ┊ ... and 2951 more.
1053485 ┊ 100.00% ┊ Σ [2961 Total Rows]
Now, let’s use binaryren’s wasm-opt tool.
wasm-opt \
--strip-debug \
-o complex.no_debug.wasm \
./target/wasm32-unknown-unknown/release/complex.wasm
Then:
twiggy top -n 10 complex.nodebug.wasm
Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼─────────────────────
75273 ┊ 16.18% ┊ data[0]
5498 ┊ 1.18% ┊ code[1410]
5478 ┊ 1.18% ┊ code[424]
4409 ┊ 0.95% ┊ code[1369]
4348 ┊ 0.93% ┊ elem[0]
4214 ┊ 0.91% ┊ code[383]
4069 ┊ 0.87% ┊ code[340]
3459 ┊ 0.74% ┊ code[385]
2892 ┊ 0.62% ┊ code[1382]
2707 ┊ 0.58% ┊ code[1552]
352801 ┊ 75.85% ┊ ... and 2933 more.
465148 ┊ 100.00% ┊ Σ [2943 Total Rows]
Reduces the fize size to: 465,148 (~20% of original)
And, lastly, optimized aggressively for size (using wasm-opt this time):
wasm-opt \
-Oz \
-o complex.size.wasm \
complex.no_debug.wasm
Then:
twiggy top -n 10 output.wasm
Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼─────────────────────
46380 ┊ 11.90% ┊ data[36]
6072 ┊ 1.56% ┊ data[58]
5843 ┊ 1.50% ┊ data[14]
5412 ┊ 1.39% ┊ code[332]
5071 ┊ 1.30% ┊ code[978]
4168 ┊ 1.07% ┊ code[942]
4014 ┊ 1.03% ┊ elem[0]
3824 ┊ 0.98% ┊ code[291]
3547 ┊ 0.91% ┊ code[259]
3311 ┊ 0.85% ┊ data[10]
302236 ┊ 77.52% ┊ ... and 1874 more.
389878 ┊ 100.00% ┊ Σ [1884 Total Rows]
Reduces the fize size to: 389,878 (~16 of original)
There are even more optimisations described in the book but, I think ~16% is a sufficiently dramatic improvement to call it quits.