
Life after wasm-pack: an opinionated deconstruction
Published on:Table of Contents
Wasm-pack, the rustwasm working group, and other Wasm related tools were sunset and archived in July 20251, after more than 5 years of being on life support. Thank you to all who made Wasm and Rust what it is today: a tech stack that continues to enthrall me.
Despite the years of ongoing maintenance issues, I and others continued to use wasm-pack as it’s recommended by the wasm-bindgen book2 (though that could be changing3) and wasm-pack has tangible benefits to ease onboarding:
- Installing the
wasm32-unknown-unknown
target - Installing the version of wasm-bindgen-cli that matched the version of wasm-bindgen used within the project
- Installing binaryen’s wasm-opt for further wasm optimizations
- Facilitating testing by instructing cargo to use the correct test runner and installing web drivers like chromedriver and geckodriver.
- Autogenerate a package.json for JS consumption.
But with its future stewardship in question, wasm-pack drawbacks are more apparent:
- The wasm-opt installed suffers from pathological performance issues on linux due to the musl allocator4. I always recommend projects pin to a known good wasm-opt version. A co-worker who used the default wasm-opt for wasm-pack, saw 10x longer build times.
- Lack of custom cargo profile support. The feature was merged 10 months ago5, but remains unreleased.
- The autogenerated package.json is nearly never what you want. Either you are better off with a handcrafted package.json or one isn’t necessary. By extension, the wasm-pack packaging and publishing becomes fruitless.
- Lack of parallelism. Wasm-pack is geared towards a serial pipeline of
cargo build
+wasm-bindgen
+wasm-opt
of a single wasm artifact. If a project has multiple Wasm bundles, it’s easier to achieve better build times with a singlecargo build
and fan out bindgen and optimization.
Nothing would please me more than to see a renaissance in wasm-pack as combining Rust + Wasm + JS ecosystems can be inscrutable at times. If only to see wasm Ferris (the wasm-pack logo featured in the article banner) grow in popularity!
It is time to let go and shatter wasm-pack into its pieces.
Dev environment
Our first task in replacing wasm-pack with its parts is to ensure easy dev environment setup, which will require downloading prebuilt binaries and installing them for all code contributor platforms. Sometimes a bash script could work. Sometimes Nix could work. But if you need to support native windows development, a different solution is needed.
Enter mise. It allows us to declaratively list dependencies for our project. Below is a config to install wasm-bindgen and wasm-opt.
[tools]
"ubi:rustwasm/wasm-bindgen" = { version = "0.2.100", extract_all = "true" }
"ubi:WebAssembly/binaryen" = { version = "version_123", extract_all = "true", bin_path = "bin" }
Mise supports installing node, rust, pnpm, and anything else in the registry or that has prebuilt executables on their github releases. When a project needs a dozen tools, automating this install is priceless.
For small Wasm projects, I use the following setup:
[tools]
"node" = { version = "22.18.0" }
"rust" = { version = "1.88.0", targets = "wasm32-unknown-unknown" }
"ubi:rustwasm/wasm-bindgen" = { version = "0.2.100", extract_all = "true" }
"ubi:WebAssembly/binaryen" = { version = "version_123", extract_all = "true", bin_path = "bin" }
For Rust installs, mise doesn’t do anything fancy other than installing rustup, as the actual management of the Rust installation is still delegated to rustup6. The config will take care of adding the wasm32-unknown-unknown
target, just like wasm-pack
.
For Github actions, it’s incredibly easy to get started:
- uses: jdx/mise-action@v2
- run: rustup target add wasm32-unknown-unknown
And if you’re asking yourself “wait, why are we explicitly adding the wasm32 target? I thought Mise would take care of it.” Don’t worry you are not alone. My hypothesis is that if Rust is installed on the system (like it is in Github actions), Mise will not add the additional targets. Probably not working as designed.
So Mise is not perfect. It’s in rapid development and should not be considered stable. There was a period of time where a race condition caused installation corruption (so pin your Mise version). Outside of Github CI, Mise will also often run afoul of Github anonymous API rate limits. The lockfile under development7 should solve this problem.
Despite these drawbacks, I think Mise is worth these growing pains for one main reason: Mise is also a task runner. Mise tasks allow me the ability to express “if the host machine is windows and we are compiling for Wasm, please swap out the C/C++ dependency for pure rust”. This is needed as MSVC does not support compiling to Wasm8, making it impossible to build the rust zstd bindings9. With Mise, I workaround the problem with the task defined below, which changes execution based on if the host platform is windows.
[tasks."build:wasm:cargo"]
description = "Build wasm"
run = [
"cargo build --release --no-default-features --features zstd_c --target wasm32-unknown-unknown",
]
run_windows = [
"cargo build --release --target wasm32-unknown-unknown",
]
As far as I know, it is impossible to express this build bifurcation using just the tools that Cargo and Rust expose.
After a cargo build, comes wasm-bindgen-cli to properly digest the output.
wasm-bindgen --target web ./target/wasm32-unknown-unknown/release/your_package.wasm --out-dir ./src/app/wasm
Not terribly complicated. If there is a version mismatch between the library and cli, wasm-bindgen-cli will throw an error. The worst part is remembering the correct profile to invoke wasm-bindgen on. Oh and always use --target web
10
Finally, call wasm-opt
with your desired optimization levels (wasm-pack
defaults to -O
) and you will have deconstructed wasm-pack build
!
In the future, if a migration away from Mise is warranted, I might grumble at my bad choices, but at least I’ll have a list of software and their versions required to drive the project.
Testing
Instead of replacing wasm-pack test
directly, I like to ask myself a series of questions:
Am I confident that my rust unit and integration tests alongside my JS unit and integration tests provide sufficient coverage? If so, I do not write wasm-bindgen tests.
When wasm-bindgen tests are desirable, update .cargo/config.toml
to point to the correct test runner:
[target.wasm32-unknown-unknown]
runner = "wasm-bindgen-test-runner"
If the tests can run in node.js, there’s a good chance it’s already installed in a wider context of a web app. Or perhaps deno, which is easily installable (via mise) and has more browser APIs baked in.
Otherwise if the wasm-bindgen tests leverage browser APIs absent in node.js and are incompatible with Deno, then we need to use a browser. While wasm-pack has support for configuring a test environment for safari, firefox, and chrome, I believe focussing on firefox gets us 99% of the way there.
Why firefox? Geckodriver is easy to add to our mise config:
[tools]
"ubi:mozilla/geckodriver" = { version = "0.36.0" }
And we can update our .cargo/config.toml
to point to the geckodriver
[target.wasm32-unknown-unknown]
runner = "wasm-bindgen-test-runner"
[env]
GECKODRIVER = "geckodriver"
If the wasm-bindgen tests stress browser differences, then these tests are better housed in JS integration tests.
I accept full responsibility if this causes an uptick in firefox installs, which is required to be installed alongside geckodriver.
In the end running the wasm-bindgen tests should be as easy as:
cargo test --target wasm32-unknown-unknown
My personal belief is that wasm-bindgen tests can be important but are niche. Even if the code being tested uses a browser API, it might not be possible to test within the context of the wasm-bindgen test harness. For instance, consider web workers, an API with a notorious ergonomics story11. It’s not feasible to serve up a web worker without a richer test harness (I like vitest/browser).
Reflections on the ecosystem
With wasm-pack replaced, what are my thoughts on the ecosystem as a whole in the context of Wasm and the browser?
Critically, wasm-bindgen has a healthy stewardship and Rust’s wasm32-unknown-unknown target sees activity (like the recent C ABI changes12).
Sometimes it seems like server side Wasm is receiving the bulk of the limelight, so I have a wishlist:
- Allowing atomics on wasm32-unknown-unknown13 would unlock more performant multithreading on stable Rust.
- Would love for browsers to get onboard the Wasm Component Model to better support web apps that want to run plugins. Extism exists, but seems like a decision that can’t be easily reversed.
Overall, I think the ecosystem is still making forward progress. A couple of months ago Leptos demonstrated Wasm code splitting with a good DX14. Perhaps this is a sign of things to come?
So while at first glance it can be disheartening to hear about sunsets and archivals of projects, I’m glad the decision was made so the community can move forward, coalesce around foundational tools, and allow room for new entrants. Like being introduced to Mise, which allows projects fine grain declaration of tooling instead of relying on the arbitrary versions within a given wasm-pack release. Will Mise be for everyone? No, but it’s a local maxima to get native Windows, macos, and linux contributors up to speed.
All I can say is that I’m excited to continue to use Wasm for the foreseeable future!
-
https://blog.rust-lang.org/inside-rust/2025/07/21/sunsetting-the-rustwasm-github-org/ ↩︎
-
https://wasm-bindgen.github.io/wasm-bindgen/introduction.html ↩︎
-
Is the use of wasm-pack still recommended in the document (Github) ↩︎
-
Default musl allocator considered harmful (to performance) ↩︎
-
Does Microsoft Visual C++ support WebAssembly as a target? (Stackoverflow) ↩︎
-
https://blog.rust-lang.org/2025/04/04/c-abi-changes-for-wasm32-unknown-unknown/ ↩︎
Comments
If you'd like to leave a comment, please email [email protected]