Setting up CI/CD for Rust Wasm projects
This guide gives you a single, copy-pasteable GitHub Actions workflow that compiles a Rust crate to
wasm32-unknown-unknown, caches the expensive steps, packages it with wasm-pack, shrinks it with
wasm-opt, runs it in real browsers, validates the binary, and deploys it — reproducibly, on every push.
Prerequisites
- [ ] A Rust crate with
crate-type = ["cdylib", "rlib"]and awasm-bindgendependency - [ ] A committed
rust-toolchain.tomlpinning the channel and thewasm32-unknown-unknowntarget - [ ]
wasm-pack0.13.x and awasm-bindgen-cliversion matching yourCargo.lock - [ ]
binaryen(wasm-opt) andwasm-tools(wasm-validate) for the optimize and validate steps - [ ] A GitHub repository with Actions enabled and a deploy target (Pages, a CDN, or an artifact store)
Procedure
1. Pin the toolchain in-repo
Commit a rust-toolchain.toml so every runner resolves the same compiler instead of a moving stable.
[toolchain]
channel = "1.83.0"
targets = ["wasm32-unknown-unknown"]
2. Lay out the workflow trigger and job
Create .github/workflows/wasm.yml. Trigger on pushes and pull requests so every change is gated.
name: wasm-ci
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
3. Cache the registry and build directory
Key the cache on the OS, the lockfile hash, and the toolchain file. Including rust-toolchain.toml in the
hash is what prevents a stale target/ from being restored after a compiler bump.
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: wasm-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}
restore-keys: |
wasm-${{ runner.os }}-
4. Install pinned tooling
Install wasm-pack and a wasm-bindgen-cli whose version exactly matches the wasm-bindgen crate in
Cargo.lock. A mismatch here is the single most common source of a broken module.
- name: Install tooling
run: |
cargo install wasm-pack --version 0.13.1 --locked
cargo install wasm-bindgen-cli --version 0.2.100 --force
sudo apt-get update && sudo apt-get install -y binaryen wabt
5. Build, optimize, and validate
Build with wasm-pack, shrink with wasm-opt -Oz, then assert structural validity with wasm-validate
before anything downstream trusts the artifact.
- name: Build and optimize
run: |
wasm-pack build --target web --release --out-dir pkg
wasm-opt pkg/*_bg.wasm -Oz --strip-debug -o pkg/optimized_bg.wasm
mv pkg/optimized_bg.wasm pkg/$(basename pkg/*_bg.wasm)
wasm-validate pkg/*_bg.wasm
6. Run headless browser tests
wasm-pack test --headless instantiates the module in real engines and asserts the ES module resolves.
- name: Test in browsers
run: wasm-pack test --headless --chrome --firefox
7. Gate and deploy
Upload the validated pkg/ and deploy it only from main, so pull requests are tested but never publish.
- uses: actions/upload-artifact@v4
with:
name: wasm-pkg
path: pkg/
- name: Deploy
if: github.ref == 'refs/heads/main'
run: npx wrangler pages deploy pkg --project-name my-wasm-app
Expected output
A successful run prints the wasm-pack and wasm-opt summaries, then the validation line. The cache
restore at the top of a warm run is the tell that step 3 is working:
Cache restored from key: wasm-Linux-3f9a...
[INFO]: Compiling to Wasm...
Compiling my-wasm-app v0.1.0
[INFO]: :-) Done in 9.42s
[INFO]: :-) Your wasm pkg is ready to publish at ./pkg.
wasm-opt: shrank 184.2 KB -> 71.8 KB
module is structurally valid
running 3 tests
test result: ok. 3 passed; 0 failed
Gotchas
RuntimeError: invalid magic number in the browser. The runner’s
~/.cargo/bin/wasm-bindgen is stale relative to the wasm-bindgen crate in Cargo.lock. Force a
matching reinstall in CI: cargo install wasm-bindgen-cli --version 0.2.100 --force. Without --force,
cargo install skips the already-present binary and the mismatch persists.
error: failed to select a version ... --locked. Your Cargo.lock is out of date with Cargo.toml.
Run cargo update -p <crate> locally, commit the regenerated lockfile, and re-push — do not drop
--locked, because that would let CI silently resolve different versions than your machine.
wasm-opt: out of memory mid-pipeline. Hosted runners cap at ~7 GB and -Oz on a large module can
abort. Confirm the optimizer is the cause with wasm-pack build --dev (which skips wasm-opt); if that
passes, move the optimize step to a larger runner or pin a Binaryen version known to fit.
Cache hit but a different binary than local. The cache key omitted the toolchain. Add
rust-toolchain.toml to hashFiles(...) so a compiler bump invalidates the target/ cache instead of
restoring object files compiled by the old toolchain.
Performance note
The ~/.cargo plus target/ cache is the difference between a cold and a warm run. A cold build of a
small wasm-bindgen crate on ubuntu-latest spends most of its time compiling dependencies — commonly
60–90 seconds. With the registry and target/ directory restored, the same job recompiles only the
changed crate and finishes in roughly 10–15 seconds, a 5–8× wall-time reduction on the build step alone.
The cache restore itself costs a second or two, so it pays for itself after the first dependency.
Frequently Asked Questions
Do I need a matrix across operating systems for a Rust Wasm build?
Not for correctness of the artifact, which is host-independent, but a matrix across ubuntu-latest,
macos-latest, and windows-latest catches toolchain and path bugs that only appear on one OS. Start
single-OS, add the matrix when portability matters — the
Cross-Platform Build Automation
guide shows the full matrix form.
Can I skip wasm-opt in CI to save time?
For pull-request builds, yes — wasm-pack build --dev skips it and gives faster feedback. Keep the full
-Oz pass on the main deploy path so what you ship is the optimized, size-gated binary.
How do I block merges that bloat the .wasm?
Capture the post-compression size with brotli -c pkg/*_bg.wasm | wc -c, compare it to the main
baseline in a PR step, and fail when it regresses beyond your budget.
Related
- Reducing Wasm bundle size with wasm-opt — the optimizer passes this pipeline runs.
- Best practices for wasm-pack configuration — tuning the build the workflow invokes.
- Rust to Wasm compilation guide — the toolchain underpinning these jobs.
← Back to Cross-Platform Build Automation