01b — Hello World Explicit + gzip
Same as 01a — explicit routes, hand-written service worker — plus one optimisation: the WASM binary is shipped gzipped and decompressed in the browser. The Go code is byte-identical to 01a; all the interesting work happens in the SW bootstrap.
Why do this?
A Go WASM "hello world" is ~11 MB uncompressed and ~2.8 MB gzipped. Production static hosts (GitHub Pages, Cloudflare Pages, Netlify) negotiate Content-Encoding: gzip for you and you don't have to care. Hosts that don't — or when you want explicit control over which variant the client gets — this pattern serves a raw .wasm.gz, decompresses it client-side, and feeds the bytes to the SW.
DecompressionStream, a named Cache entry whose URL must match the SW's resolved main.wasm path, and a hand-set Content-Type. Hiding those behind a flag makes debugging awful; showing them turns them into exhibits. If any of the pieces goes wrong, the symptom (usually a stuck "Compiling WASM..." spinner) is much easier to diagnose when you can read the five-step promise chain in front of you.
The five-step bootstrap
Read go/templates/sw/index.html alongside this. Each numbered comment in that file corresponds to one step below:
- Fetch
main.wasm.gzas-is. The file is served withContent-Type: application/gzip(notapplication/wasm+Content-Encoding: gzip), so the browser hands us the raw compressed bytes instead of auto-decoding. - Pipe through
DecompressionStream('gzip'). The browser's native streaming decompressor. Output is aReadableStreamof raw WASM bytes, consumed vianew Response(...).blob(). - Cache the decompressed blob. Open
caches.open('wasm-gz-01b'),cache.put(wasmUrl, new Response(blob, {headers: {'Content-Type': 'application/wasm'}})). Two details matter here:- The cache name is example-scoped (
wasm-gz-01b) so other gzipped examples can't collide. - The URL key is resolved from
location.href, so it matches whateverregisterWasmHTTPListener('main.wasm', …)asks for at runtime.
- The cache name is example-scoped (
- Register the service worker with
cacheName: 'wasm-gz-01b'. The go-wasm-http-server runtime reads from the named cache first and only falls back to the network if that's a miss. Because step 3 just wrote a match, the SW finds the bytes instantly. - Poll
./favicon.ico. The SW needs one fetch to instantiate the WASM and register its handler. Favicon is the cheapest probe; when it comes back 200, redirect to./which the SW now serves from the Go app.
// go/templates/sw/index.html — the core promise chain
fetch('main.wasm.gz')
.then(resp => {
const ds = new DecompressionStream('gzip');
return new Response(resp.body.pipeThrough(ds)).blob();
})
.then(blob => caches.open('wasm-gz-01b').then(cache => {
const wasmUrl = new URL('main.wasm', location.href).href;
return cache.put(wasmUrl, new Response(blob, {
headers: {'Content-Type': 'application/wasm'}
}));
}))
.then(() => navigator.serviceWorker.register('sw.js'))
.then(() => navigator.serviceWorker.ready)
.then(() => waitForWasm());
What the SW itself looks like
Almost the same as 01a — just two lines different (the cacheName and the variant-specific cache scope):
// go/templates/sw/sw.js
importScripts('wasm_exec.js');
importScripts('wasmhttp_sw.js');
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', e => e.waitUntil(clients.claim()));
registerWasmHTTPListener('main.wasm', {
cacheName: 'wasm-gz-01b', // ← the one meaningful line vs 01a
passthrough: function(request) {
var url = new URL(request.url);
if (url.hostname !== self.location.hostname) return true;
if (url.pathname.endsWith('/index.html')) return true;
return false;
}
});
Recovering from a stuck service worker
Every SW-based demo in this repo ships a tiny recovery stub next to its main entry point. For 01b that's wasm_demo/demo-sw.html — a static HTML page served by the host (not the SW), which runs a short script to unregister any SW whose scope is a prefix of the current URL and then redirects to the canonical ./sw/ entry.
If the demo gets stuck ("Compiling WASM..." spinning indefinitely, or a blank page), navigate to:
/01b_hello_world_explicit_gzip/wasm_demo/demo-sw.html
That path is outside the SW's sw/ scope, so the static host serves it unconditionally. It cleans up any ancestor-scoped registration and redirects — the bootstrap page it lands on does its own unregister-before-register, so a child-scoped stale SW gets replaced too. If even that fails, you're in DevTools territory (Application → Service Workers → Unregister + Cache Storage → Delete wasm-gz-01b).
Failure modes to know about
| Symptom | Usual cause |
|---|---|
| Stuck "Compiling WASM..." forever | Cache URL mismatch. The SW is asking for …/main.wasm but the bootstrap wrote to a different URL. Make sure new URL('main.wasm', location.href).href resolves the same way in both places. |
| "Incorrect response MIME type" error | Forgot to set Content-Type: application/wasm on the cached Response. WebAssembly.instantiateStreaming refuses to parse non-wasm content types. |
| Works once, fails after reload | Stale cached blob from a previous build. Bump the cache name (append a version), or clear site data in DevTools. |
| Works in Chrome, stuck in Firefox | Usually a loading-order issue — register the SW only AFTER cache.put resolves, and use updateViaCache: 'none' to prevent HTTP-cache shadowing. |
Running
# Server mode (no gzip involved — server serves the normal binary)
task go-example:01b
# Opens http://localhost:1342
# WASM demo (via docs)
task docs:build-wasm
tp pages
# Navigate to http://localhost:8080/01b_hello_world_explicit_gzip/wasm_demo/sw/