Deployment and Production
Getting WebAssembly applications into production is where theory meets reality. I’ve deployed dozens of WebAssembly apps, and each deployment taught me something new about what works in the real world versus what works on my development machine.
The biggest surprise? Performance characteristics change dramatically between development and production environments. That blazing-fast WebAssembly module on localhost might crawl over a slow network connection.
Build Pipeline Setup
My production build process evolved through painful trial and error. Initially, I just ran go build
and called it done. That approach failed spectacularly when users complained about 10MB download sizes.
Here’s the build pipeline I use now:
// build.go - Custom build script
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// Set WebAssembly build environment
os.Setenv("GOOS", "js")
os.Setenv("GOARCH", "wasm")
// Build with optimizations
cmd := exec.Command("go", "build",
"-ldflags", "-s -w", // Strip debug info
"-o", "dist/main.wasm",
"main.go")
if err := cmd.Run(); err != nil {
fmt.Printf("Build failed: %v\n", err)
os.Exit(1)
}
fmt.Println("Build completed successfully")
}
The -ldflags "-s -w"
flags strip debug information and symbol tables, reducing file size by 30-40%. For a 5MB module, that’s 1.5-2MB savings - significant for users on slow connections.
Compression Strategies
WebAssembly modules compress extremely well. I’ve seen 80% size reductions with proper compression configuration.
My nginx configuration for serving WebAssembly:
location ~* \.wasm$ {
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 9;
gzip_types application/wasm;
# Enable Brotli if available
brotli on;
brotli_comp_level 11;
brotli_types application/wasm;
# Cache for 1 year with proper headers
expires 1y;
add_header Cache-Control "public, immutable";
}
Brotli compression typically achieves 10-15% better compression than gzip for WebAssembly modules. The difference between a 2MB and 1.7MB download matters on mobile networks.
Loading Strategy Implementation
The loading strategy makes or breaks user experience. I learned this when users abandoned my app because it showed a blank screen for 8 seconds while the WebAssembly module loaded.
My current loading approach:
class WasmLoader {
constructor() {
this.module = null;
this.loading = false;
}
async loadWithProgress(wasmUrl, onProgress) {
if (this.loading) return;
this.loading = true;
try {
// Fetch with progress tracking
const response = await fetch(wasmUrl);
const contentLength = response.headers.get('content-length');
const total = parseInt(contentLength, 10);
let loaded = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
if (onProgress) {
onProgress(loaded, total);
}
}
// Combine chunks and instantiate
const wasmBytes = new Uint8Array(loaded);
let offset = 0;
for (const chunk of chunks) {
wasmBytes.set(chunk, offset);
offset += chunk.length;
}
const go = new Go();
const result = await WebAssembly.instantiate(wasmBytes, go.importObject);
this.module = result.instance;
go.run(result.instance);
return this.module;
} finally {
this.loading = false;
}
}
}
This loader provides progress feedback and handles network interruptions gracefully. Users see a progress bar instead of a blank screen.
Error Handling in Production
Production environments surface errors that never appear in development. Network timeouts, memory constraints, and browser compatibility issues all require specific handling.
My error handling strategy:
type ErrorHandler struct {
fallbackMode bool
}
func (eh *ErrorHandler) HandlePanic(this js.Value, args []js.Value) interface{} {
if r := recover(); r != nil {
// Log error details
js.Global().Get("console").Call("error",
fmt.Sprintf("WebAssembly panic: %v", r))
// Switch to fallback mode
eh.fallbackMode = true
// Notify JavaScript layer
js.Global().Call("wasmErrorCallback", map[string]interface{}{
"error": fmt.Sprintf("%v", r),
"fallback": true,
})
return "Error handled, switched to fallback"
}
return "No error"
}
func (eh *ErrorHandler) IsInFallbackMode(this js.Value, args []js.Value) interface{} {
return eh.fallbackMode
}
The fallback mode ensures the application remains functional even when WebAssembly fails. Critical for production reliability.
Performance Monitoring
Production performance monitoring revealed patterns invisible during development. Memory usage spikes, garbage collection pauses, and performance degradation over time all became apparent only under real user loads.
type PerformanceMonitor struct {
startTime time.Time
operations int
memoryPeaks []int
}
func (pm *PerformanceMonitor) StartOperation(this js.Value, args []js.Value) interface{} {
pm.startTime = time.Now()
return "Operation started"
}
func (pm *PerformanceMonitor) EndOperation(this js.Value, args []js.Value) interface{} {
duration := time.Since(pm.startTime)
pm.operations++
// Report to analytics
js.Global().Call("reportPerformance", map[string]interface{}{
"operation": args[0].String(),
"duration": duration.Milliseconds(),
"count": pm.operations,
})
return duration.Milliseconds()
}
This monitoring helped identify that certain operations became slower after 1000+ iterations due to memory fragmentation.
CDN Configuration
Serving WebAssembly modules through CDNs requires specific configuration. Standard CDN settings often don’t work well for WebAssembly.
My CloudFront configuration:
{
"Origins": [{
"DomainName": "myapp.com",
"CustomOriginConfig": {
"HTTPPort": 443,
"OriginProtocolPolicy": "https-only"
}
}],
"DefaultCacheBehavior": {
"TargetOriginId": "myapp-origin",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "custom-wasm-policy",
"Compress": true
},
"CustomErrorResponses": [{
"ErrorCode": 404,
"ResponseCode": 200,
"ResponsePagePath": "/fallback.html"
}]
}
The custom cache policy ensures WebAssembly modules cache properly while allowing for updates when needed.
Security Considerations
WebAssembly security in production requires attention to several vectors. Content Security Policy, CORS headers, and input validation all need careful configuration.
CSP configuration for WebAssembly:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
object-src 'none';">
The 'wasm-unsafe-eval'
directive is required for WebAssembly but should be used carefully.
Rollback Strategy
WebAssembly deployments need rollback strategies. Unlike JavaScript, WebAssembly modules can’t be easily hot-swapped if issues arise.
My rollback approach:
const WASM_VERSIONS = {
'v1.2.0': '/wasm/v1.2.0/main.wasm',
'v1.1.0': '/wasm/v1.1.0/main.wasm',
'v1.0.0': '/wasm/v1.0.0/main.wasm'
};
async function loadWasmWithFallback() {
const versions = Object.keys(WASM_VERSIONS);
for (const version of versions) {
try {
const module = await loadWasm(WASM_VERSIONS[version]);
console.log(`Loaded WebAssembly version: ${version}`);
return module;
} catch (error) {
console.warn(`Failed to load ${version}, trying next...`);
}
}
throw new Error('All WebAssembly versions failed to load');
}
This approach automatically falls back to previous versions if the latest deployment fails.
Monitoring and Alerting
Production WebAssembly applications need comprehensive monitoring. I track load times, error rates, and performance metrics across different browsers and devices.
Key metrics I monitor:
- Load Success Rate: Percentage of successful WebAssembly loads
- Load Time P95: 95th percentile load time across all users
- Memory Usage: Peak memory consumption during operations
- Error Rate: Frequency of WebAssembly-related errors
- Browser Compatibility: Success rates across different browsers
Deployment Checklist
Before each production deployment, I run through this checklist:
- Build Optimization: Confirm debug symbols stripped, compression enabled
- Performance Testing: Verify performance on slow networks and devices
- Error Handling: Test fallback modes and error recovery
- Security Review: Validate CSP headers and input sanitization
- Monitoring Setup: Ensure all metrics and alerts are configured
- Rollback Plan: Confirm previous version available for quick rollback
Common Production Issues
Issues I’ve encountered in production:
- Memory Leaks: Go finalizers not running, causing memory growth
- Network Timeouts: Large modules failing to load on slow connections
- Browser Crashes: Memory-intensive operations crashing mobile browsers
- CORS Errors: CDN configuration blocking WebAssembly loads
- Cache Issues: Stale WebAssembly modules causing compatibility problems
Each issue taught me something about production WebAssembly deployment that I couldn’t learn in development.
The next and final part will cover advanced techniques and the future of Go WebAssembly development.