Go on WebAssembly: Browser, WASI, and Embedded Paths
Go compiles to WebAssembly, but WebAssembly is not one deployment model.
Search across all documentation pages
Go compiles to WebAssembly, but WebAssembly is not one deployment model.
The same language reaches the browser DOM, server-side WASI hosts, and flash-constrained boards through different GOOS/GOARCH pairs and sometimes a different compiler (TinyGo).
Choosing the path early determines binary size, stdlib coverage, and which host runtime you embed.
js/wasm), WASI server (wasip1/wasm), and embedded/constrained (TinyGo) - each with different syscalls, hosts, and size budgets.syscall/js where a WASI module should run headless.syscall/js, WASI preview 1 (wasip1), host runtime, import surface, TinyGo -target, linear memory..wasm, FaaS/edge workers, or firmware-adjacent WASM on devices.net/http pattern ports unchanged.WebAssembly is a portable bytecode format with a linear memory model and an import/export ABI.
Go does not emit a standalone OS - it emits a .wasm module that imports host functions for I/O (files, clocks, randomness, DOM, sockets).
The Go compiler target (GOOS + GOARCH, or TinyGo -target) decides which imports the linker expects and which stdlib packages compile.
Browser path (GOOS=js GOARCH=wasm): Go runs inside a JavaScript host.
The historical model uses syscall/js to call DOM APIs and register callbacks.
The host loads wasm_exec.js (from GOROOT/misc/wasm) to implement the Go runtime's JS imports.
This path is for interactive UIs and WASM-in-the-tab demos, not for headless servers.
WASI path (GOOS=wasip1 GOARCH=wasm): Go compiles to a headless module that speaks WASI Preview 1 - a POSIX-ish capability surface (files, environment, clocks, random).
You run the .wasm with a host runtime such as wasmtime, wazero, or Spin.
This is the default mental model for "Go binary, but portable WASM" on servers, CI sandboxes, and edge platforms.
Embedded / constrained path (TinyGo): TinyGo is a separate compiler that targets microcontrollers (ARM Cortex-M, AVR, RISC-V) and also wasm with much smaller binaries.
It trades stdlib completeness and compile-time features for flash/RAM budgets.
Use it when the device or WASM download size is the hard constraint.
Your Go source
|
+-- go build (gc) -- GOOS=js GOARCH=wasm --> browser + syscall/js
|
+-- go build (gc) -- GOOS=wasip1 GOARCH=wasm --> WASI .wasm + host runtime
|
+-- tinygo build -- -target=... --> MCU firmware or tiny WASM
Import surface is the contract between module and host.
Browser modules import JS runtime shims; WASI modules import wasi_snapshot_preview1 functions; TinyGo WASM may import a minimal set tuned for size.
If you compile for wasip1 but load the module in a browser without WASI polyfills, instantiation fails at import resolution.
Memory and runtime: Full Go WASM includes the Go scheduler and GC in the module.
Linear memory grows as the heap grows.
Go 1.26 allocates heap in smaller chunks for sub-16 MiB heaps, which materially reduces idle memory for WASI microservices and small filters.
TinyGo uses a different runtime with lower baseline memory, at the cost of goroutine/reflection limits.
Build tags and file splits: Browser code often lives behind //go:build js && wasm so server packages do not pull syscall/js into WASI builds.
WASI code uses //go:build wasip1 (or the default non-JS build) and should avoid DOM-specific packages entirely.
Host runtimes differ in embedding ergonomics:
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
js/wasm + syscall/js | DOM access from Go | Large binary, JS glue required | Browser demos, WASM UI widgets |
wasip1/wasm (gc toolchain) | Full stdlib, familiar go build | Multi-MiB modules typical | Server plugins, portable CLIs, wazero embedding |
| TinyGo WASM / MCU | Very small binaries | Incomplete stdlib, fewer libs | Embedded boards, size-capped edge WASM |
Polyglot WASM pipelines: Teams often author hot paths in Rust for minimum size and orchestration in Go via wazero.
Go 1.26's heap improvements narrow the gap for small services that benefit from staying in one language.
Security and sandboxing: WASI capability-based I/O fits multi-tenant hosts better than giving modules raw host filesystem access.
Pre-open directories and environment variables explicitly in wasmtime/wazero config rather than assuming POSIX parity.
CI strategy: Build matrices should include GOOS=wasip1 GOARCH=wasm (and TinyGo if used) alongside linux/amd64.
Run modules with wasmtime run or wazero in integration tests to catch missing imports early.
Deprecation awareness: Browser-first Go WASM relying on syscall/js is a specialized niche.
Greenfield server work should default to wasip1 unless DOM bridging is the actual requirement.
"One .wasm runs everywhere" - Import sets differ per target; a browser module is not interchangeable with a WASI module without recompilation and a matching host.
"TinyGo is just -ldflags -s -w for Go" - TinyGo uses a different compiler and runtime; language support and stdlib coverage diverge materially.
"WASI equals full Linux" - WASI preview 1 covers files, clocks, and randomness, not a complete network stack on every host.
Check your runtime's socket and HTTP extensions before porting net/http servers verbatim.
Keep browser builds isolated behind build tags.
Use GOOS=wasip1 and GOARCH=wasm with the standard Go toolchain.
This produces a WASI Preview 1 module you run with wasmtime, wazero, or compatible edge hosts.
When Go must call browser APIs (DOM, fetch, canvas) via syscall/js.
For compute-only workloads in the browser, consider compiling to WASI and using a host with WASI-in-JS, or keep the hot path in JavaScript/TypeScript.
Networking depends on the host runtime and WASI version.
Many hosts expose sockets through extensions; Spin maps HTTP triggers directly.
Prototype on your target host before assuming ListenAndServe works unchanged.
TinyGo is an independent LLVM-based toolchain focused on small binaries.
It shares Go syntax but does not guarantee every stdlib package or reflection pattern works.
Full Go modules include runtime, scheduler, and GC.
TinyGo and import minimization shrink size; Go 1.26 heap chunking reduces memory overhead but not necessarily initial download size without -ldflags=-s -w.
wazero when the embedder is already Go (no cgo, easy CI).
wasmtime for CLI/dev parity and broad WASI coverage.
Spin when deploying HTTP handlers to Fermyon Cloud or compatible edge runtimes.
No. wasm_exec.js is for the js/wasm target.
WASI modules run under a WASM runtime that provides WASI imports, not the browser JS shim.
Place browser-only code in files tagged js && wasm, shared logic in default builds, and keep main packages separate per target if imports diverge.
Yes for memory: smaller heap increments help sub-16 MiB workloads.
The compiler also always uses certain WASM instruction extensions (signext, satconv settings are ignored).
TinyGo with an explicit -target for your board (for example arduino, pico, cortex-m variants).
Full go build does not emit firmware images for those devices.
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).
Reviewed by Chris St. John·Last updated Jul 16, 2026