Wasm in Production — Junior Level¶
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Pros & Cons
- Use Cases
- Code Examples
- Coding Patterns
- Clean Code
- Product Use / Feature
- Error Handling
- Security Considerations
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Common Misconceptions
- Tricky Points
- Test
- Tricky Questions
- Cheat Sheet
- Self-Assessment Checklist
- Summary
- What You Can Build
- Further Reading
- Related Topics
- Diagrams & Visual Aids
Introduction¶
Focus: "I compiled a
.wasmand it runs on my laptop — what changes when real users load it?"
By now you can run GOOS=js GOARCH=wasm go build and open the result in a browser, or build a wasip1 module and run it with wazero. That is the development story. This file is about the production story: what changes the moment the module leaves your machine and a real server delivers it to a real user, or a real Go service loads someone else's module as a plugin.
The list of concerns is short but unforgiving:
- The server must serve
.wasmfiles with the headerContent-Type: application/wasm. Get this wrong and the browser refuses to stream-compile the module — it falls back to a slower path, or fails outright. - A Go-compiled
.wasmis big — typically a few megabytes. You must compress it (Gzip or Brotli) and show the user a loading indicator while it downloads. - The little JavaScript glue file
wasm_exec.jsmust match the exact Go version that built the module. A mismatch silently breaks things. - On the server side, if you embed other people's Wasm inside your Go program, you are running untrusted code. It must be sandboxed: memory limits, time limits, no surprise file or network access.
After reading this file you will: - Know what Content-Type: application/wasm is for and what breaks without it - Serve a Go .wasm from a tiny Go HTTP server, compressed, with a loading indicator - Understand why wasm_exec.js must be pinned to your Go version - Have a first, honest picture of server-side Wasm with wazero - Know when Wasm is the right tool — and when it is overkill
You do not yet need to understand fuel metering, capability threat models, or edge cold-start economics. Those are the next levels. This file is about the moment you say: "ship it."
Prerequisites¶
- Required: You can already build a Go
.wasmfor the browser. See 01-goos-js-wasm-browser. - Required: Basic HTTP knowledge — requests, responses, headers, status codes, and what a
Content-Typeheader does. - Required: Comfort writing a small Go HTTP server (
net/http). - Helpful: A first look at WASI /
wasip1, the server-side Wasm target. See 02-wasi-and-wasip1. - Helpful: Familiarity with browser DevTools — the Network tab especially.
If you can build main.wasm and you know how to write http.HandleFunc, you are ready.
Glossary¶
| Term | Definition |
|---|---|
.wasm file | The compiled WebAssembly binary. For browser Go, produced by GOOS=js GOARCH=wasm go build. |
Content-Type: application/wasm | The MIME type a server must send for .wasm files so the browser knows it is WebAssembly. |
| Streaming compilation | The browser compiling the module while it downloads, via WebAssembly.instantiateStreaming. Requires the correct MIME type. |
wasm_exec.js | The JavaScript glue shipped with the Go toolchain that bridges the Go runtime to the browser. Must match the Go version. |
| Gzip / Brotli | HTTP compression schemes. Wasm compresses very well — often to 25–35% of original size. |
| Content hash | A filename like app.a1b2c3.wasm derived from the file's contents, so caches never serve a stale version. |
| CDN | Content Delivery Network — edge servers that cache and serve static assets close to users. |
| wazero | A pure-Go, zero-dependency WebAssembly runtime that lets a Go host program run Wasm guest modules. |
| Host / Guest | In server-side Wasm: the host is your Go program; the guest is the Wasm module it loads and runs. |
| Sandbox | The boundary that prevents a guest module from touching memory, files, or the network unless explicitly allowed. |
WASI / wasip1 | A standard system interface for Wasm outside the browser. wasip1 is the version Go targets with GOOS=wasip1 GOARCH=wasm. |
Core Concepts¶
The MIME type is not cosmetic¶
When the browser fetches a .wasm file, it decides how to compile it based on the Content-Type header. The fast path — WebAssembly.instantiateStreaming(fetch(url), importObject) — compiles the module as the bytes arrive over the network, overlapping download and compilation. But it only works if the response says Content-Type: application/wasm.
If the server sends application/octet-stream or text/plain, the browser throws:
TypeError: Failed to execute 'compileStreaming' on 'WebAssembly':
Incorrect response MIME type. Expected 'application/wasm'.
Good code falls back to the non-streaming path (download fully, then compile) — but that is slower and wastes the streaming advantage. The fix is one line of server configuration. Always serve .wasm as application/wasm.
Go .wasm files are large, and compression matters¶
A trivial "hello world" Go program compiled to browser Wasm is ~2 MB. A real application can be 5–15 MB. That is because the Go runtime — the garbage collector, the scheduler, reflection — is baked into every binary.
The good news: WebAssembly is highly compressible. A 5 MB .wasm typically shrinks to 1.3–1.8 MB with Gzip, and a bit smaller still with Brotli. Compression is the single highest-leverage thing you can do for load time. Serving a multi-megabyte .wasm uncompressed is the most common production mistake.
(If 2 MB for hello-world horrifies you, that is what TinyGo is for — it produces far smaller browser binaries by dropping parts of the runtime. TinyGo is a separate toolchain with its own tradeoffs; see 03-tinygo-for-wasm-and-embedded. This file is about standard Go.)
Show the user something while it loads¶
A 1.5 MB download on a slow connection takes seconds. During that time, an unstyled blank page is a bad experience. Always render a loading indicator before you start fetching the Wasm, and hide it once the module is running.
wasm_exec.js must match your Go version¶
wasm_exec.js is the JavaScript shim that wires up the Go runtime: it implements the functions the Go .wasm calls to talk to the browser (console output, timers, the DOM bridge). It ships inside your Go installation:
# Go 1.24+
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .
# Older Go (1.21–1.23)
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
The shim and the .wasm are a matched pair. If you upgrade Go and rebuild the .wasm but keep an old wasm_exec.js, the runtime contract breaks — sometimes loudly, sometimes silently. Copy a fresh wasm_exec.js every time you change Go versions, and pin both together.
Two very different "productions"¶
"Wasm in production" splits into two worlds that share almost nothing operationally:
- Browser delivery. You build a
.wasm, a web server (or CDN) ships it to browsers, and it runs in the page. The concerns are HTTP: MIME, compression, caching, loading UX. - Server-side embedding. Your Go service loads a
.wasmmodule with wazero and runs it as a sandboxed plugin. The concerns are isolation: memory, time, and capability limits on untrusted code.
This file introduces both. Most teams need only one of them. Know which you are doing.
Server-side Wasm: the host runs the guest¶
With wazero, a Go program is the host. It reads a .wasm file (the guest), instantiates it inside a sandbox, and calls its exported functions. The guest cannot read your files, open sockets, or see your memory — unless you explicitly grant it those capabilities. That deny-by-default isolation is the whole point: it lets you run code you did not write and do not fully trust, safely, in the same process.
Real-World Analogies¶
1. The shipping container. A .wasm module is a sealed shipping container. It does not matter what is inside — the crane (the runtime) handles it the same way. The MIME type is the label on the side that tells the dock crane "this is a container, use the container crane (streaming)" rather than "unknown cargo, unload it by hand."
2. Vacuum-packing for the journey. A raw Go .wasm is a fluffy 5 MB. Compression vacuum-packs it to under 2 MB for transit, and the browser re-inflates it on arrival. Shipping it uncompressed is like mailing a pillow without packing it down — you pay for all that air.
3. A guest in a glass room. Server-side Wasm runs a guest module in a glass-walled room: you can watch it, it can do its work, but the doors are locked. It cannot wander into your kitchen (filesystem) or use your phone (network) unless you hand it a key. That is the sandbox.
4. The matched key and lock. wasm_exec.js and the .wasm are a key and its lock, cut from the same blank (your Go version). A key from a different blank may turn — or may jam in a way you only discover at the worst moment.
Mental Models¶
Model 1 — Production Wasm is a delivery problem first¶
In the browser, 90% of "production readiness" is HTTP hygiene: correct MIME, compression, caching, a loading state. The Go code barely changes between dev and prod; the serving is what makes or breaks the experience.
Model 2 — The byte journey¶
[build] main.wasm (5 MB)
│ brotli/gzip
▼
[ship] main.wasm.br (1.6 MB) over HTTPS, Content-Type: application/wasm
│ browser decompresses + streams-compiles
▼
[run] instantiated module executing in the page
Every box in that chain is a place to get it right or wrong.
Model 3 — Host owns the sandbox¶
Server-side, the host (your Go program) is the landlord. The guest module rents a room with exactly the furniture (memory, time, capabilities) the landlord provides — nothing more. Deny-by-default means the empty room is the default, and you add furniture deliberately.
Model 4 — wasm_exec.js + .wasm = one artifact¶
Treat the glue file and the binary as a single deployable unit, versioned together. Never let them drift.
Model 5 — Most apps do not need Wasm¶
A plain CRUD web app — forms, tables, server-rendered HTML — has no reason to ship a multi-megabyte Wasm runtime to the browser. Wasm earns its weight when you need real compute in the client (image processing, a parser, a simulation) or a sandbox on the server (untrusted plugins). If you cannot name the compute or the sandbox, you probably do not need Wasm.
Pros & Cons¶
Pros¶
- Real CPU-bound work in the browser — image filters, video frames, parsers, simulations — at near-native speed.
- Reuse Go code on the client — share validation, parsing, or domain logic between server and browser.
- Safe extensibility on the server — run third-party or user-supplied logic in a sandbox, in-process, without spinning up containers.
- Portability — the same
.wasmruns in any compliant runtime (browser, wazero, edge platform). - Fast cold starts at the edge — a Wasm module starts in microseconds-to-milliseconds, far faster than booting a container.
Cons¶
- Binary size. Standard Go browser Wasm is multi-megabyte. Compression helps but does not erase it.
- No threads (browser). Go's goroutines run cooperatively on one OS thread under
GOOS=js; no true parallelism in the browser. - Constrained I/O.
wasip1has no sockets by design; browser Wasm reaches the network only through JS. - Debugging is harder. Stack traces, source maps, and profilers are less mature than for native code.
- Operational overhead. You must get MIME, compression, caching, and version pinning right, or users get a broken or slow experience.
The honest summary: Wasm is a power tool. Reach for it when the problem fits, not because it is new.
Use Cases¶
Reach for production Wasm when:
- Client-side media processing — resize, crop, filter images or decode/encode video frames in the browser without a round-trip to the server.
- In-browser data tools — CSV/Parquet viewers, query engines, spreadsheet-grade calculation that runs locally.
- Sharing Go logic with the frontend — the exact same validation or parsing code in the browser and on the server.
- Server-side plugin systems — let customers extend your product with logic you run in a sandbox.
- Policy engines — evaluate untrusted rules (OPA-style) safely, in-process.
- Sandboxed user code execution — coding platforms, "run this snippet" features, multi-tenant compute.
Do not reach for Wasm when:
- The app is ordinary CRUD with server-rendered pages — the runtime weight buys you nothing.
- You only need a little interactivity that plain JavaScript handles in kilobytes.
- You need raw network access on the server (
wasip1has none) — a normal process is simpler.
Code Examples¶
Example 1 — A correct static server for browser Wasm (Go)¶
The minimal production-shaped server: correct MIME, and (below) compression.
package main
import (
"log"
"net/http"
)
func main() {
fs := http.FileServer(http.Dir("./public"))
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The one line everyone forgets:
if len(r.URL.Path) > 5 && r.URL.Path[len(r.URL.Path)-5:] == ".wasm" {
w.Header().Set("Content-Type", "application/wasm")
}
fs.ServeHTTP(w, r)
}))
log.Println("serving ./public on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Note: Go's own mime package maps .wasm to application/wasm since Go 1.17, so a bare http.FileServer often does the right thing on modern Go. But many other servers (older nginx, some CDNs, S3 static hosting) do not — so always verify the header in DevTools.
Example 2 — The HTML that loads it (streaming)¶
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Go Wasm</title></head>
<body>
<div id="loading">Loading… (downloading ~1.6 MB)</div>
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then((result) => {
document.getElementById("loading").remove();
go.run(result.instance);
})
.catch((err) => {
document.getElementById("loading").textContent = "Failed to load: " + err;
});
</script>
</body>
</html>
instantiateStreaming is the streaming, fast path. If the MIME type is wrong, this rejects — which is why the .catch matters.
Example 3 — Compress the .wasm at build time¶
Pre-compress once, serve the small file forever:
# Build, then compress
GOOS=js GOARCH=wasm go build -o public/main.wasm .
gzip -9 -k public/main.wasm # -> public/main.wasm.gz
# brotli, if available:
brotli -q 11 -k public/main.wasm # -> public/main.wasm.br
Then serve the pre-compressed variant when the client supports it (see middle.md for the full negotiation), or let the server compress on the fly with a compression middleware.
Example 4 — Pin wasm_exec.js in your build script¶
#!/usr/bin/env bash
set -euo pipefail
# Copy the shim that MATCHES this Go toolchain:
if [ -f "$(go env GOROOT)/lib/wasm/wasm_exec.js" ]; then
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" public/ # Go 1.24+
else
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" public/ # older Go
fi
GOOS=js GOARCH=wasm go build -o public/main.wasm .
echo "Built with $(go version)"
Run this on every build so the shim and binary never drift.
Example 5 — A first server-side Wasm host with wazero¶
The other production world: your Go program runs a guest .wasm in a sandbox.
package main
import (
"context"
"fmt"
"os"
"github.com/tetratelabs/wazero"
)
func main() {
ctx := context.Background()
// The runtime is the sandbox. Closing it frees all guest memory.
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasmBytes, err := os.ReadFile("guest.wasm")
if err != nil {
panic(err)
}
// Compile + instantiate the untrusted guest. By default it can do
// NOTHING dangerous: no files, no network, no host memory access.
mod, err := r.Instantiate(ctx, wasmBytes)
if err != nil {
panic(err)
}
// Call an exported function the guest provides.
add := mod.ExportedFunction("add")
results, err := add.Call(ctx, 2, 3)
if err != nil {
panic(err)
}
fmt.Println("guest add(2,3) =", results[0]) // 5
}
Notice what is not here: no permission to read your disk, open a socket, or touch your process memory. The guest is sandboxed by default. That deny-by-default posture is what makes server-side Wasm safe enough to run untrusted code.
Coding Patterns¶
Pattern: a build script, not a memorized command¶
Wrap the build, the shim copy, and the compression in one script (Example 4). Nobody should be hand-typing GOOS=js GOARCH=wasm go build and separately remembering to copy the shim.
Pattern: always handle the load failure¶
instantiateStreaming(...).catch(...) is not optional. The wrong MIME type, a 404, or a corrupt file should produce a visible error, not a silently dead page.
Pattern: loading indicator before the fetch¶
Render the spinner first, start the fetch second, remove the spinner in .then. Never fetch before showing the user that something is happening.
Pattern: close the wazero runtime¶
defer r.Close(ctx) frees the sandbox's memory. For server-side hosts, the runtime is a resource — treat it like a file handle.
Clean Code¶
- Serve
.wasmasapplication/wasm— verify it, do not assume it. Open DevTools → Network → click the.wasm→ checkContent-Type. - Pre-compress in the build, do not compress per request where you can — it is wasted CPU on every download otherwise.
- Keep
wasm_exec.jsandmain.wasmtogether in the same deploy, built by the same script. - Always show a loading state and always handle the failure path.
- In a wazero host, name the capabilities you grant. If you grant filesystem access, do it explicitly and narrowly — never "just to make it work."
Product Use / Feature¶
For a product team, Wasm shows up as:
- A faster, more private feature — image editing that never uploads the photo, because the processing happens in the browser.
- An extensibility story — "bring your own logic" plugins your platform runs safely.
- A shared-logic win — one Go validation library, used identically on the server and in the form, so the rules never drift.
It also shows up as a performance budget line: a multi-megabyte download that the loading UX, compression, and caching strategy must manage. Treat the .wasm payload as a first-class part of your page-weight budget, the same way you treat images and JavaScript bundles.
Error Handling¶
"Incorrect response MIME type. Expected 'application/wasm'."¶
The server is not sending Content-Type: application/wasm. Fix the server config. As a stopgap, the client can fall back to non-streaming:
async function loadGo(url, go) {
try {
const r = await WebAssembly.instantiateStreaming(fetch(url), go.importObject);
return r.instance;
} catch (e) {
// Fallback: download fully, then compile (slower).
const bytes = await (await fetch(url)).arrayBuffer();
const r = await WebAssembly.instantiate(bytes, go.importObject);
return r.instance;
}
}
The fallback hides the symptom; fix the MIME type to actually solve it.
A blank page, no errors¶
Often a wasm_exec.js mismatch after a Go upgrade. Re-copy the shim from the current GOROOT and rebuild.
404 on main.wasm¶
The file is not where the page expects it, or the deploy dropped it. Check the path and the build output.
Guest Call returns an error (server-side)¶
A wazero guest function returning an error usually means a trap inside the guest (out-of-bounds memory, divide by zero, an unmet import). The host gets a Go error — log it, do not let it crash the host.
Security Considerations¶
- Browser Wasm runs in the browser's sandbox. It cannot escape the page's security boundary any more than JavaScript can — same origin policy, same permissions. That is reassuring but also limiting (no raw sockets, no filesystem).
- Server-side guests are untrusted by default — keep them that way. With wazero, a guest can do nothing dangerous unless you grant it a capability. Grant the minimum.
- Resource limits are security. An untrusted guest can try to allocate gigabytes or spin forever. You must cap memory and impose a timeout (covered at middle.md and professional.md). At this level: know that an unbounded guest is a denial-of-service waiting to happen.
- Serve over HTTPS. A
.wasmdelivered over plain HTTP can be tampered with in transit. Production Wasm is HTTPS-only, no exceptions. - Know where your modules come from. A
.wasmyou load is code you run. Treat third-party modules with the same supply-chain caution as any dependency.
Performance Tips¶
- Compression is the biggest win. Gzip a 5 MB
.wasmto ~1.6 MB; Brotli a little smaller. Do this first, always. - Stream-compile by getting the MIME type right — overlaps download and compile.
- Cache aggressively with content-hashed filenames so repeat visits are instant (covered in middle.md).
- Lazy-load the
.wasmonly on the routes that need it — do not pay the download on your marketing homepage. - For server-side hosts, compile once, instantiate many. Compiling a module is the expensive step; reuse the compiled module across invocations.
Best Practices¶
- Always serve
.wasmasapplication/wasm, and verify in DevTools. - Always compress the
.wasm(Gzip at minimum, Brotli if you can). - Always show a loading indicator and handle the load-failure path.
- Pin
wasm_exec.jsto the building Go version via a build script. - Serve over HTTPS.
- Lazy-load Wasm only where it is needed.
- For wazero hosts, deny by default — grant capabilities explicitly and minimally.
- Don't reach for Wasm unless the problem needs it — name the compute or the sandbox.
Edge Cases & Pitfalls¶
Pitfall 1 — Wrong MIME type breaks streaming¶
The single most common production bug. Symptom: a MIME-type error in the console, or a slow fallback path. Fix: serve application/wasm.
Pitfall 2 — Shipping the .wasm uncompressed¶
A 5 MB download where 1.6 MB would do. Symptom: slow loads, especially on mobile. Fix: enable compression.
Pitfall 3 — Stale wasm_exec.js¶
You upgraded Go, rebuilt the .wasm, but kept the old shim. Symptom: blank page or runtime errors. Fix: re-copy the shim on every build.
Pitfall 4 — No loading UX¶
The page is blank for seconds while the multi-megabyte download runs. Users think it is broken. Fix: render a loading state before fetching.
Pitfall 5 — Caching the wrong version¶
You deploy a new .wasm but users keep running the old one because a CDN cached main.wasm forever. Fix: content-hashed filenames (see middle.md).
Pitfall 6 — Assuming POSIX on the server¶
A wasip1 guest cannot open a socket — there is no networking in wasip1. Code that assumes a normal OS will fail. Know the limits before you port.
Pitfall 7 — Unbounded server-side guest¶
A wazero host that sets no memory or time limit will hang or OOM when a guest misbehaves. Even at junior level, never run an untrusted guest without some limit.
Common Mistakes¶
- Forgetting the MIME type — the number-one issue.
- Serving uncompressed Wasm — the number-two issue.
- Letting
wasm_exec.jsdrift from the Go version. - No loading indicator and no error handling on the load path.
- Loading Wasm on every page instead of only where it is used.
- Running a server-side guest with no resource limits.
- Using Wasm for an app that has no real compute or sandbox need.
Common Misconceptions¶
"Wasm is faster than JavaScript, so I should use it everywhere."
For DOM manipulation and light glue, JavaScript is smaller and often faster end-to-end (no multi-megabyte download). Wasm wins for compute, not for everything.
"The browser figures out the file type automatically."
For streaming compilation, it relies on the Content-Type header. Wrong header, no streaming.
"Compression is the CDN's job; I don't need to think about it."
Many static hosts do not compress .wasm by default. Verify it, or pre-compress yourself.
"
wasm_exec.jsis generic — any copy works."
It is version-matched to the Go runtime. A mismatch breaks the contract.
"Server-side Wasm needs Docker or a separate process."
No — wazero runs the guest in-process, in pure Go, with no external runtime. That is its appeal.
"If it runs on my laptop, it is production-ready."
Your laptop sends the right MIME, has the matching shim, and has a warm connection. Production has a CDN, a cold cache, and a phone on 4G.
Tricky Points¶
- Go's
mimepackage maps.wasmcorrectly since Go 1.17, so a bare Gohttp.FileServeroften works — but nginx, S3, and older servers do not, so never assume. instantiateStreamingrejects on wrong MIME, butinstantiate(from anArrayBuffer) does not care — which is why the fallback path exists.- Compression ratio depends on the binary, but 25–35% of original is a good rule of thumb for Go Wasm.
- The browser sandbox and the wazero sandbox are different sandboxes — one is the browser protecting the user from the page; the other is your Go host protecting itself from the guest.
- TinyGo produces much smaller browser binaries but is a different toolchain with library limits — a real option, not the default. See 03-tinygo-for-wasm-and-embedded.
Test¶
Try this end to end.
mkdir wasm-prod && cd wasm-prod
go mod init example.com/wasmprod
cat > main.go <<'EOF'
//go:build js && wasm
package main
import "fmt"
func main() { fmt.Println("hello from wasm") }
EOF
mkdir public
GOOS=js GOARCH=wasm go build -o public/main.wasm .
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" public/ 2>/dev/null || \
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" public/
ls -lh public/main.wasm # note the size
gzip -9 -k public/main.wasm
ls -lh public/main.wasm.gz # note the compressed size
Now answer: 1. How big is the uncompressed .wasm? How big after Gzip? What ratio? (Answer: typically ~2 MB → ~600 KB, roughly 30%.) 2. What Content-Type must the server send for streaming to work? (Answer: application/wasm.) 3. What happens if you serve it as text/plain and use instantiateStreaming? (Answer: a MIME-type error.) 4. Why must wasm_exec.js come from the same Go version that built main.wasm? (Answer: it is the version-matched runtime shim.) 5. Could you run main.wasm on the server with wazero? (Answer: not this one — it is a js/wasm binary; you would build GOOS=wasip1 for wazero.)
Tricky Questions¶
Q1. My .wasm loads fine locally but fails on the deployed site. Why?
A. Almost always the MIME type. Your local Go server sends application/wasm; the deployed static host (nginx, S3, a CDN) does not. Check DevTools → Network → the .wasm response's Content-Type.
Q2. The page is blank after I upgraded Go. The build succeeded. What changed?
A. You rebuilt main.wasm with the new Go, but wasm_exec.js is still the old one. Re-copy the shim from $(go env GOROOT)/lib/wasm/ (or misc/wasm/ on older Go).
Q3. Do I need to compress if my CDN already does Gzip?
A. Verify it actually compresses .wasm — many CDNs only compress text MIME types by default and skip application/wasm. If it does, great. If not, pre-compress.
Q4. Can a .wasm running in the browser read the user's files or make arbitrary network calls?
A. No more than JavaScript can. It lives in the browser's sandbox — same-origin policy, file access only via user-initiated pickers, network only through fetch/JS. The browser protects the user.
Q5. Is server-side Wasm with wazero just running a subprocess?
A. No — the guest runs in-process, inside your Go program, with no external runtime, no container, no separate process. That is exactly why it is interesting for plugins.
Q6. Why is hello-world 2 MB?
A. The Go runtime (GC, scheduler, reflection) is compiled into every binary. TinyGo strips most of it for far smaller binaries, at the cost of language/library coverage.
Q7. My server-side guest hung the whole process. How do I prevent that?
A. Impose limits — a memory cap and an execution timeout (via a context with deadline, plus a memory-limited runtime config). Never run an untrusted guest unbounded. Details at middle.md and professional.md.
Q8. Should every page on my site load the .wasm?
A. No. Lazy-load it only on the routes that use it. A marketing homepage should not download a 1.6 MB compute module nobody on that page needs.
Cheat Sheet¶
# Build browser wasm + pin the matching shim
GOOS=js GOARCH=wasm go build -o public/main.wasm .
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" public/ # Go 1.24+
# (older Go: $(go env GOROOT)/misc/wasm/wasm_exec.js)
# Pre-compress
gzip -9 -k public/main.wasm # -> .gz
brotli -q 11 -k public/main.wasm # -> .br
# Verify the MIME type after deploy
curl -sI https://yoursite/main.wasm | grep -i content-type
# want: content-type: application/wasm
Browser load (the happy path):
1. render loading indicator
2. fetch main.wasm (Content-Type: application/wasm, compressed)
3. WebAssembly.instantiateStreaming(...) ← streams while downloading
4. remove indicator, go.run(instance)
5. on error: show the failure, don't die silently
| Symptom | Likely cause | Fix |
|---|---|---|
| "Incorrect response MIME type" | server not sending application/wasm | fix server config |
| Slow multi-MB load | uncompressed .wasm | enable Gzip/Brotli |
| Blank page after Go upgrade | stale wasm_exec.js | re-copy the shim |
| Stale version served | cache without content hash | hash the filename |
| Server host hung | unbounded guest | memory cap + timeout |
Self-Assessment Checklist¶
You can move on to middle.md when you can:
- Explain what
Content-Type: application/wasmenables and what breaks without it - Serve a Go
.wasmfrom a small Go HTTP server with the correct MIME type - Compress a
.wasmand state the rough ratio for Go binaries - Add a loading indicator and a load-failure handler to the page
- Explain why
wasm_exec.jsmust match the building Go version - Describe, in two sentences, the difference between browser delivery and server-side embedding
- Write a minimal wazero host that runs a guest function
- State why a server-side guest must have resource limits
- Decide whether a given feature actually needs Wasm
Summary¶
"Wasm in production" is two stories. In the browser, it is mostly a delivery problem: serve the .wasm as application/wasm so the browser can stream-compile it, compress the multi-megabyte payload (Gzip to ~30% of original, a bit less with Brotli), show a loading indicator, handle failures, and keep wasm_exec.js pinned to the Go version that built the binary. On the server, it is an isolation problem: with wazero, your Go host runs a guest .wasm in a deny-by-default sandbox, which is exactly what makes it safe to run untrusted plugin code in-process — provided you impose resource limits.
Most apps need neither. Wasm earns its weight when you have real client-side compute or a real server-side sandboxing need. Get the delivery hygiene right, keep the sandbox locked down, and ship deliberately.
What You Can Build¶
After learning this:
- A static site that serves a Go
.wasmcorrectly — right MIME, compressed, with a loading state. - An in-browser tool (a small image filter, a CSV viewer) that does real compute client-side.
- A first sandboxed plugin host with wazero that runs a guest module's exported function safely.
- A build script that pins
wasm_exec.jsand compresses the binary so deploys are reproducible.
You cannot yet: - Negotiate Brotli/Gzip per request or wire up content-hashed caching (next: middle.md) - Build a real multi-tenant plugin architecture with fuel and memory limits (next: middle.md, senior.md) - Reason about edge/serverless cold-start economics (next: senior.md) - Design the capability threat model for untrusted modules (next: professional.md)
Further Reading¶
- wazero documentation — the zero-dependency pure-Go Wasm runtime.
- Go WebAssembly wiki — official browser-Wasm guidance.
- MDN: WebAssembly.instantiateStreaming — streaming compilation.
- MDN: Loading and running WebAssembly code — the MIME-type requirement.
- WASI / wasip1 overview — the server-side target.
Related Topics¶
- 16.1 GOOS=js Wasm in the Browser — building the browser
.wasmyou ship here - 16.2 WASI and wasip1 — the server-side target wazero runs
- 16.3 TinyGo for Wasm and Embedded — smaller browser binaries, different tradeoffs
- 16.4 Wasm Interop and Performance — crossing the JS↔Go boundary efficiently
Diagrams & Visual Aids¶
The two production worlds:
BROWSER DELIVERY SERVER-SIDE EMBEDDING
---------------- ---------------------
Go build (js/wasm) Go build (wasip1/wasm) — the GUEST
│ │
▼ ▼
web server / CDN your Go service (the HOST + wazero)
serves .wasm loads guest into a SANDBOX
Content-Type: application/wasm memory + time + capability limits
compressed (gzip/brotli) │
│ ▼
▼ calls guest's exported functions
browser stream-compiles (untrusted, isolated)
+ runs in the page
The byte journey (browser):
main.wasm (5 MB)
│ brotli -q 11
▼
main.wasm.br (1.6 MB)
│ HTTPS, Content-Type: application/wasm
▼
browser: decompress + instantiateStreaming
│
▼
running module ✓
The matched pair — never let them drift:
Go 1.24 toolchain
│
├── builds ──▶ main.wasm ┐
│ ├── deploy together
└── ships ──▶ wasm_exec.js┘
In this topic
- junior
- middle
- senior
- professional