WebAssembly & TinyGo Best Practices
Testing WASM builds in CI and avoiding syscall/js in server modules.
These rules keep WASM artifacts portable, sandbox-friendly, and sized appropriately from first spike through production hosts.
How to Use This List
- Apply during design review when a feature mentions WASM, TinyGo, wazero, or edge deploy.
- Add
GOOS=wasip1 GOARCH=wasmcompile andwasmtime runsmoke steps to CI for guest packages. - Pair with the Native Go vs TinyGo vs Rust WASM Decision Guide when toolchain choice is unsettled.
- Revisit after Go minor upgrades; WASM runtime behavior evolves (Go 1.26 heap chunking is a recent example).
A - Target and build hygiene
- Default server modules to
wasip1/wasm. Reservejs/wasmfor browser DOM requirements only. - Never import
syscall/jsin WASI packages. Enforce withjs && wasmbuild tags on browser files. - Strip release WASM with
-ldflags="-s -w". Keep unstripped artifacts in CI storage for crash triage. - Pin toolchain versions in CI images. Record Go 1.26.x, TinyGo, wasmtime, and wazero together in the build log.
- Audit
go list -depsfor guest mains. Reject accidentalnet/http/pprofor test-only imports in plugin entrypoints.
B - CI and testing
- Compile WASM on every PR touching guest code.
GOOS=wasip1 GOARCH=wasm go build ./...catches import regressions early. - Smoke-run guests with wasmtime. Non-zero exit codes should fail the pipeline like native binaries.
- Mirror wazero embed tests if production hosts in Go. CLI success does not guarantee embed configuration is correct.
- Add TinyGo build jobs when TinyGo ships to prod.
tinygo build -target=wasmmay fail while gc builds pass. - Log artifact sizes in CI. Track gc vs TinyGo bytes to detect dependency bloat over time.
C - Host runtime and sandboxing
- Pre-open directories with least privilege. Map only required host paths into guest namespaces (
--dir/ wazeroFSConfig). - Pass environment explicitly. Do not rely on implicit host env parity between dev laptops and production.
- Bound guest execution with context deadlines in wazero. Prevent runaway CPU in multi-tenant embedders.
- Cache compiled modules in long-running hosts. Re-instantiating cold compiles dominates latency.
- Choose wazero for pure-Go embedders; wasmtime for CLI/dev parity. Document why if you pick otherwise.
D - Size and toolchain selection
- Measure size before choosing gc vs TinyGo. Compare
ls -lhon the same feature slice, not hello-world only. - Re-evaluate gc WASM on Go 1.26+ when memory was the blocker. Smaller heap chunks help dense wazero pools.
- Use TinyGo for MCU and sub-megabyte edge caps. Verify stdlib and reflect limitations with a compile spike first.
- Avoid fat JSON/reflection frameworks in TinyGo guests. Prefer small structs and hand-tuned marshaling.
- Document per-plugin toolchain in plugin registry metadata. Operators should know which compiler built each
.wasm.
E - Browser and syscall/js
- Version
wasm_exec.jswith your Go toolchain. Mismatch causes obscure import failures in browsers. - Call
js.Func.Release()when removing listeners. Prevent leaks on long-lived SPA pages. - Block
mainwithselect {}in browser guests. Exitingmaintears down the runtime before callbacks fire. - Serve
.wasmasapplication/wasm. Fix static server MIME maps forinstantiateStreaming. - Keep DOM surface area small. Prefer TS/React for layout; WASM for compute when possible.
F - Operations and security
- Treat WASM as immutable versioned artifacts. Publish hashes; roll forward by swapping module bytes deliberately.
- Do not mount host root into guests. One compromised plugin should not read the entire filesystem.
- Align WASI capability grants across dev and prod. Drift here is a common "works locally" failure mode.
- Upgrade Go for security fixes in crypto guests. Pin to 1.26.x and rebuild WASM on security releases.
- Run
go fixmodernizers on shared packages. Go 1.26go fixkeeps code idiomatic before cross-compiling to WASM.
FAQs
What is the single most common WASM mistake in Go repos?
Compiling server plugins to js/wasm or leaking syscall/js into wasip1 builds.
Default to wasip1 and enforce build tags in review.
Minimum CI for WASM?
GOOS=wasip1 GOARCH=wasm go build plus wasmtime run on at least one representative guest binary per module.
When must TinyGo appear in CI?
Whenever production deploy artifacts are produced with TinyGo, including MCU firmware and size-capped edge WASM.
How does Spin change practices?
Add spin build and route tests; HTTP triggers may replace raw net/http listeners in guests.
Should I pin wazero API versions?
Yes in go.mod alongside Go 1.26.x.
Runtime APIs evolve; upgrades should run embed integration tests.
Are GOWASM toggles still relevant on 1.26?
signext and satconv are ignored per release notes.
Read current docs each upgrade instead of copying old flags.
How do I test filesystem guests locally?
Use wasmtime run --dir $(pwd)::/work (or tighter paths) and keep the same mounts in wazero ModuleConfig.
What belongs in an ADR?
Chosen toolchain, size/memory measurements, rejected alternatives, host runtime, and capability matrix for FS/env/network.
Related
- Compiling with GOOS=wasip1 GOARCH=wasm - build commands
- WASI Host Runtimes: wasmtime, wazero & Spin - host configuration
- syscall/js: Go in the Browser - browser-only interop
- Go 1.26 WASM Runtime & Memory Improvements - upgrade triggers
- WebAssembly & TinyGo Basics - hands-on starting points
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).