WebAssembly Beyond the Browser

July 26, 2021

WebAssembly started as a browser technology—a compilation target for running C++ games in web pages. But Wasm’s properties (sandboxed, fast, portable) make it compelling beyond browsers. Server-side Wasm, edge computing, and plugin systems are emerging.

Here’s where WebAssembly is heading and why it matters.

Why Wasm Beyond the Browser?

Key Properties

wasm_properties:
  sandboxed:
    - Memory isolated
    - No system access by default
    - Capabilities must be granted
    - Safe to run untrusted code

  portable:
    - Same binary runs everywhere
    - No recompilation needed
    - Works across OS and CPU

  fast:
    - Near-native performance
    - Predictable execution
    - No JIT warmup needed
    - Small binary size

  language_agnostic:
    - Compile from Rust, C/C++, Go, etc.
    - Bring your own language
    - Consistent interface

WASI: The System Interface

wasi_overview:
  purpose: Standard system interface for Wasm
  capabilities:
    - File system access (sandboxed)
    - Network (emerging)
    - Environment variables
    - Clocks and random

  design:
    - Capability-based security
    - Must grant permissions explicitly
    - No ambient authority

Server-Side Wasm

Why Use Wasm on Servers?

server_side_benefits:
  isolation:
    - Stronger than containers
    - Faster startup than VMs
    - Memory-safe by default

  multi_tenancy:
    - Run untrusted tenant code safely
    - Resource limits per instance
    - No container overhead

  polyglot:
    - Mix languages in one service
    - Rust module + Python module
    - Common runtime

Wasm Runtimes

runtimes:
  wasmtime:
    language: Rust
    focus: Production, standards compliance
    features:
      - WASI support
      - Component model
      - Cranelift JIT

  wasmer:
    language: Rust
    focus: Universal Wasm runtime
    features:
      - Multiple backends
      - Package manager (WAPM)
      - Language integrations

  wazero:
    language: Go
    focus: Zero dependencies
    features:
      - Pure Go implementation
      - No CGO needed
      - Good for Go applications

Embedding Wasm in Go

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/wasi_snapshot_preview1"
)

func main() {
    ctx := context.Background()

    // Create runtime
    runtime := wazero.NewRuntime(ctx)
    defer runtime.Close(ctx)

    // Instantiate WASI
    wasi_snapshot_preview1.MustInstantiate(ctx, runtime)

    // Load Wasm module
    wasmBytes, _ := os.ReadFile("plugin.wasm")

    // Configure with limited capabilities
    config := wazero.NewModuleConfig().
        WithStdout(os.Stdout).
        WithArgs("plugin", "arg1")

    // Run
    module, _ := runtime.InstantiateWithConfig(ctx, wasmBytes, config)

    // Call exported function
    fn := module.ExportedFunction("process")
    result, _ := fn.Call(ctx, 42)
    fmt.Printf("Result: %d\n", result[0])
}

Embedding Wasm in Rust

use wasmtime::*;

fn main() -> Result<()> {
    let engine = Engine::default();
    let module = Module::from_file(&engine, "plugin.wasm")?;

    let mut linker = Linker::new(&engine);
    wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;

    let wasi = WasiCtxBuilder::new()
        .inherit_stdio()
        .build();

    let mut store = Store::new(&engine, wasi);
    let instance = linker.instantiate(&mut store, &module)?;

    // Call exported function
    let process = instance.get_typed_func::<i32, i32>(&mut store, "process")?;
    let result = process.call(&mut store, 42)?;
    println!("Result: {}", result);

    Ok(())
}

Edge Computing with Wasm

Cloudflare Workers

// Workers use V8 isolates, not Wasm directly
// But you can use Wasm modules within Workers

// Compile Rust to Wasm
// wasm-pack build --target web

import init, { process_data } from './pkg/my_module.js';

export default {
  async fetch(request, env) {
    await init();

    const data = await request.json();
    const result = process_data(data.input);

    return new Response(JSON.stringify({ result }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

Fastly Compute@Edge

// Rust -> Wasm for Fastly

use fastly::{Error, Request, Response};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    // Route based on path
    match req.get_path() {
        "/api/transform" => handle_transform(req),
        _ => Ok(Response::from_status(404)),
    }
}

fn handle_transform(req: Request) -> Result<Response, Error> {
    let body = req.into_body_str();
    let result = transform_data(&body);

    Ok(Response::from_body(result)
        .with_content_type(fastly::mime::APPLICATION_JSON))
}

Edge Performance

edge_wasm_benefits:
  cold_start:
    - Wasm: ~1ms
    - Container: 100ms-seconds
    - Critical for edge latency

  memory:
    - Smaller than containers
    - More instances per node
    - Better resource utilization

  security:
    - Sandboxed execution
    - Safe for multi-tenant
    - No privilege escalation

Plugin Systems

Wasm for Extensibility

plugin_use_cases:
  envoy_proxy:
    - Custom filters in Wasm
    - Deploy without rebuilding Envoy
    - Isolated from proxy memory

  databases:
    - User-defined functions
    - Custom aggregations
    - Safe execution

  applications:
    - Third-party extensions
    - Customer customization
    - Sandboxed plugins

Envoy Wasm Filters

// Rust Wasm filter for Envoy
use proxy_wasm::traits::*;
use proxy_wasm::types::*;

struct HttpHeaders;

impl Context for HttpHeaders {}

impl HttpContext for HttpHeaders {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // Add header to all requests
        self.add_http_request_header("x-custom-header", "value");

        // Log request
        if let Some(path) = self.get_http_request_header(":path") {
            log::info!("Request to: {}", path);
        }

        Action::Continue
    }

    fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
        // Modify response
        self.set_http_response_header("x-processed-by", "wasm-filter");
        Action::Continue
    }
}

Application Plugin System

// Host application loading Wasm plugins
type PluginHost struct {
    runtime wazero.Runtime
    plugins map[string]api.Module
}

func (h *PluginHost) LoadPlugin(name, path string) error {
    wasmBytes, err := os.ReadFile(path)
    if err != nil {
        return err
    }

    // Define host functions available to plugin
    _, err = h.runtime.NewHostModuleBuilder("host").
        NewFunctionBuilder().
        WithFunc(func(ctx context.Context, m api.Module, ptr, len uint32) {
            // Host function implementation
            data := readMemory(m, ptr, len)
            h.handlePluginEvent(name, data)
        }).
        Export("emit_event").
        Instantiate(context.Background())

    // Load plugin module
    module, err := h.runtime.Instantiate(context.Background(), wasmBytes)
    if err != nil {
        return err
    }

    h.plugins[name] = module
    return nil
}

func (h *PluginHost) CallPlugin(name string, input []byte) ([]byte, error) {
    module := h.plugins[name]

    // Allocate memory in Wasm module
    alloc := module.ExportedFunction("alloc")
    ptr, _ := alloc.Call(context.Background(), uint64(len(input)))

    // Write input to Wasm memory
    module.Memory().Write(uint32(ptr[0]), input)

    // Call plugin function
    process := module.ExportedFunction("process")
    result, _ := process.Call(context.Background(), ptr[0], uint64(len(input)))

    // Read result from Wasm memory
    return readResult(module, result)
}

Component Model

The Future of Wasm Interop

component_model:
  purpose: High-level interface types for Wasm
  features:
    - Rich types (strings, records, lists)
    - Interface definitions (WIT)
    - Language bindings generation
    - Module composition

  current_status: Proposal, implementations emerging

WIT Interface Example

// example.wit
package my:component

interface processor {
    record input-data {
        id: string,
        values: list<f64>,
    }

    record output-data {
        id: string,
        result: f64,
        metadata: option<string>,
    }

    process: func(data: input-data) -> result<output-data, string>
}

world example {
    export processor
}

Challenges

current_challenges:
  ecosystem:
    - Tooling still maturing
    - Debugging is difficult
    - Profiling tools limited

  performance:
    - Host function calls expensive
    - Memory copying overhead
    - Not always faster than native

  capabilities:
    - Networking still emerging in WASI
    - Threads support varies
    - File system access limited

  size:
    - Rust binaries can be large
    - Need optimization passes
    - Debug builds much larger

Key Takeaways

WebAssembly is becoming a universal runtime. Not for everything, but for specific use cases it’s compelling.