Skip to content
Published on

WebAssembly & エッジコンピューティング: ブラウザAI推論、Cloudflare Workers、IoT Wasmまで

Authors

WebAssemblyとは何か

WebAssembly(Wasm)は2019年にW3C公式Web標準として採択されたバイナリ命令フォーマットです。ブラウザ、サーバー、IoTデバイスを問わずネイティブに近い速度で実行でき、JavaScriptがブラウザで動く唯一の言語という地位を事実上終わらせました。

Wasmのコア設計原則は以下の4つです。

  • 安全性(Safety): サンドボックスメモリモデルがホスト環境を保護します。
  • 移植性(Portability): CPUアーキテクチャに関係なく同一の動作を保証します。
  • 速度(Speed): JIT/AOTコンパイルにより、数値演算でJavaScriptの数倍のスループットを実現します。
  • 開放性(Open): 特定の言語やプラットフォームに依存しません。

1. WATテキストフォーマットとバイトコード構造

WebAssemblyには2つの表現形式があります。.wasmバイナリフォーマットと、人間が読める.wat(WebAssembly Text Format)です。

WAT例: 2つの整数の和

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add)))

.wasmにコンパイルされるとマジックナンバー\0asm(0x00 0x61 0x73 0x6D)で始まるバイナリになります。

バイトコード構造

Wasmモジュールはセクション単位で構成されています。

セクションID名前説明
1Type関数シグネチャ定義
3Function関数インデックス
7Export外部に公開するシンボル
10Code実際の関数ボディ

線形メモリ(Linear Memory)

Wasmは単一の連続バイト配列である線形メモリを使用します。64KBページ単位で割り当てられ、JavaScriptからWebAssembly.Memoryオブジェクトを通じて直接アクセスできます。

const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 })
const buffer = new Uint8Array(memory.buffer)
// オフセット0から直接読み書き
buffer[0] = 42

ポインタ演算はWasmインスタンスのサンドボックス内に安全に隔離されており、ホストメモリには一切アクセスできません。


2. WASI: WebAssembly System Interface

WASIはWasmモジュールがファイルシステム、ネットワーク、環境変数などのOS機能にアクセスするための標準システムインターフェースです。Dockerの創設者Solomon Hykes氏が「WASM+WASIが2008年に存在していたらDockerは必要なかった」と述べたことで広く知られるようになりました。

(import "wasi_snapshot_preview1" "fd_write"
  (func $fd_write (param i32 i32 i32 i32) (result i32)))

WASIが提供する主要な抽象化:

  • ファイルシステム: fd_readfd_writepath_open
  • クロック: clock_time_get
  • 環境変数: environ_get
  • ネットワーク(WASI 0.2): wasi:socketsインターフェース

2024年に正式リリースされたWASI 0.2(Component Model)では、高水準インターフェース定義言語WIT(Wasm Interface Types)が導入されました。


3. Wasmエコシステム: Rust、AssemblyScript、Emscripten

Rust to WebAssembly (wasm-pack)

RustはWasmエコシステムで最も成熟した言語です。wasm-packを使えばnpmパッケージとしてすぐ配布できるWasmモジュールを生成できます。

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[wasm_bindgen]
pub fn matrix_multiply(a: &[f32], b: &[f32], n: usize) -> Vec<f32> {
    let mut result = vec![0.0f32; n * n];
    for i in 0..n {
        for j in 0..n {
            for k in 0..n {
                result[i * n + j] += a[i * n + k] * b[k * n + j];
            }
        }
    }
    result
}

ビルドとデプロイ:

# wasm-packのインストール
cargo install wasm-pack

# ブラウザ向けビルド
wasm-pack build --target web

# Node.js向けビルド
wasm-pack build --target nodejs

生成されたpkg/ディレクトリには.wasmバイナリ、JavaScriptグルーコード、TypeScript型定義が含まれます。

JavaScriptからWasmモジュールを呼び出す

import init, { fibonacci, matrix_multiply } from './pkg/my_module.js'

async function main() {
  // Wasmモジュールの初期化
  await init()

  // フィボナッチ計算
  console.log(fibonacci(40)) // 102334155

  // 行列乗算 (4x4)
  const a = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
  const b = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
  const result = matrix_multiply(a, b, 4)
  console.log(result)
}

main()

AssemblyScript

TypeScriptに似た構文でWasmを記述できる言語です。

// assembly/index.ts
export function add(a: i32, b: i32): i32 {
  return a + b
}

export function sumArray(ptr: usize, len: i32): i64 {
  let sum: i64 = 0
  for (let i = 0; i < len; i++) {
    sum += load<i32>(ptr + i * 4)
  }
  return sum
}

Emscripten (C/C++)

C/C++コードベースをWasmにポーティングする際のツールチェーンです。FigmaとGoogle EarthはこのアプローチでWasmを使用しています。

# CファイルをWasmにコンパイル
emcc compute.c -O3 -o compute.js \
  -s WASM=1 \
  -s EXPORTED_FUNCTIONS='["_process_image"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'

4. ブラウザAI: ONNX Runtime Web、WebNN、WebGPU

ONNX Runtime Webによるブラウザ内推論

ONNX Runtime WebはブラウザでONNXモデルを直接実行します。バックエンドとしてWebAssembly(CPU)、WebGL、WebGPUをサポートします。

import * as ort from 'onnxruntime-web'

async function runInference() {
  // WebGPUバックエンド優先、フォールバックはWasm
  const session = await ort.InferenceSession.create('/models/bert-base.onnx', {
    executionProviders: ['webgpu', 'wasm'],
    graphOptimizationLevel: 'all',
  })

  // 入力テンソルの作成
  const inputIds = new BigInt64Array([101n, 2023n, 2003n, 102n])
  const attentionMask = new BigInt64Array([1n, 1n, 1n, 1n])

  const feeds = {
    input_ids: new ort.Tensor('int64', inputIds, [1, 4]),
    attention_mask: new ort.Tensor('int64', attentionMask, [1, 4]),
  }

  const results = await session.run(feeds)
  console.log('Logits:', results.logits.data)
}

WebGPUコンピュートシェーダーによる行列演算

WebGPUはGPUの並列演算能力をWebから直接活用できます。ML推論においてWebGLより優れている核心的な理由はコンピュートシェーダーのファーストクラスサポートです。

async function webgpuMatmul(matA, matB, M, N, K) {
  const adapter = await navigator.gpu.requestAdapter()
  const device = await adapter.requestDevice()

  const shaderCode = `
    @group(0) @binding(0) var<storage, read> matA: array<f32>;
    @group(0) @binding(1) var<storage, read> matB: array<f32>;
    @group(0) @binding(2) var<storage, read_write> result: array<f32>;

    @compute @workgroup_size(8, 8)
    fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
      let row = gid.x;
      let col = gid.y;
      var sum = 0.0;
      for (var k = 0u; k < ${K}u; k++) {
        sum += matA[row * ${K}u + k] * matB[k * ${N}u + col];
      }
      result[row * ${N}u + col] = sum;
    }
  `

  const shaderModule = device.createShaderModule({ code: shaderCode })
  // ... バッファ作成、パイプライン設定、ディスパッチ
}

WebNN API

WebNN(Web Neural Network API)はブラウザがOSレベルのハードウェアアクセラレーション(NPU、GPU、DSP)を直接活用できるW3C標準APIです。

const context = await navigator.ml.createContext({ deviceType: 'gpu' })
const builder = new MLGraphBuilder(context)

const input = builder.input('input', { type: 'float32', dimensions: [1, 3, 224, 224] })
const weights = builder.constant(/* ... */)
const conv = builder.conv2d(input, weights, { padding: [1, 1, 1, 1] })
const relu = builder.relu(conv)

const graph = await builder.build({ output: relu })
const results = await context.compute(graph, inputs, outputs)

5. エッジAIデプロイ: Cloudflare Workers、Fastly、AWS Lambda@Edge

Cloudflare Workers AI

Cloudflare WorkersはV8 isolateベースで世界300以上のPoP(Point of Presence)で実行されます。AIバインディングによりエッジで直接推論を行います。

// Cloudflare Worker with AIバインディング
export default {
  async fetch(request, env) {
    const body = await request.json()
    const userMessage = body.message

    // AIバインディングでLLM推論
    const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
      messages: [
        {
          role: 'system',
          content: 'You are a helpful assistant.',
        },
        {
          role: 'user',
          content: userMessage,
        },
      ],
      max_tokens: 512,
    })

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

wrangler.toml設定:

name = "edge-ai-worker"
main = "src/index.js"
compatibility_date = "2024-09-23"

[ai]
binding = "AI"

Fastly Compute (RustベースのWasm)

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

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    let body = req.into_body_str();

    // Wasm内で直接処理
    let processed = process_at_edge(&body);

    Ok(Response::from_body(processed))
}

fn process_at_edge(input: &str) -> String {
    format!("Processed at edge: {}", input.to_uppercase())
}

Cloudflare Workers vs AWS Lambda@Edge 比較

特性Cloudflare WorkersAWS Lambda@Edge
実行モデルV8 Isolateコンテナベース
コールドスタート約0ms100ms〜数秒
メモリ上限128MB128MB〜10GB
最大実行時間30秒(有料プラン)30秒
グローバルPoP300以上CloudFrontエッジ
言語サポートJS/TS、WasmNode.js、Python、Javaなど

Cloudflare Workersのコールドスタートがほぼゼロな核心的理由は、新しいOSプロセスを起動するのではなくV8 isolateを再利用するからです。各WorkerはOSプロセスの初期化を伴わず、同一プロセス内の隔離されたJavaScript実行コンテキストで動作します。


6. IoT & 組み込みWasm

WasmEdge

WasmEdgeはCNCF(Cloud Native Computing Foundation)のサンドボックスプロジェクトで、IoTおよびエッジデバイス向けの軽量Wasmランタイムです。

# WasmEdgeのインストール
curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash

# WasmEdgeでPythonスクリプトを実行
wasmedge --dir .:. python_wasm.wasm script.py

WasmEdge上でPythonスクリプトを実行:

# script.py - WasmEdge上で動作
import sys
import json

def process_sensor_data(data):
    temperature = data.get('temperature', 0)
    humidity = data.get('humidity', 0)

    if temperature > 80 or humidity > 90:
        return {'alert': True, 'reason': 'threshold_exceeded'}
    return {'alert': False, 'status': 'normal'}

data = json.loads(sys.argv[1])
result = process_sensor_data(data)
print(json.dumps(result))

WAMR (WebAssembly Micro Runtime)

WAMRはBytecode Allianceが開発した超軽量Wasmランタイムで、数KBのRAMで動作します。

最小メモリ要件:
- インタープリターモード: 約85KB ROM + 64KB RAM
- AOTモード: 約60KB ROM + 64KB RAM

Fermyon Spin

SpinはWasmベースのマイクロサービスフレームワークです。

# spin.toml
spin_manifest_version = 2

[application]
name = "iot-processor"
version = "0.1.0"

[[trigger.http]]
route = "/sensor"
component = "sensor-handler"

[component.sensor-handler]
source = "target/wasm32-wasi/release/sensor_handler.wasm"
[component.sensor-handler.build]
command = "cargo build --target wasm32-wasi --release"

7. パフォーマンスベンチマーク: Wasm vs ネイティブ

WasmのSIMD

Wasm SIMDは128ビットベクトル演算をサポートし、MLワークロードを大幅に高速化します。

// RustでWasm SIMDを活用
#[cfg(target_arch = "wasm32")]
use std::arch::wasm32::*;

pub fn dot_product_simd(a: &[f32], b: &[f32]) -> f32 {
    let mut sum = f32x4_splat(0.0);
    let chunks = a.len() / 4;

    for i in 0..chunks {
        let va = v128_load(a[i*4..].as_ptr() as *const v128);
        let vb = v128_load(b[i*4..].as_ptr() as *const v128);
        sum = f32x4_add(sum, f32x4_mul(va, vb));
    }

    // 水平合算
    let arr: [f32; 4] = unsafe { std::mem::transmute(sum) };
    arr.iter().sum()
}

SharedArrayBufferによるマルチスレッディング

Wasmのスレッド化はSharedArrayBufferAtomics APIを活用します。

// 共有メモリでWasmマルチスレッディング
const sharedMemory = new WebAssembly.Memory({
  initial: 16,
  maximum: 256,
  shared: true, // SharedArrayBufferを有効化
})

// Workerに共有メモリを渡す
const worker = new Worker('wasm-worker.js')
worker.postMessage({ memory: sharedMemory })

ベンチマーク結果(参考値)

タスクJavaScriptWasm(単体)Wasm + SIMDネイティブC
行列乗算(1024x1024)850ms210ms55ms40ms
SHA-256ハッシュ(1MB)120ms35ms22ms18ms
画像リサイズ(4K)340ms95ms28ms20ms

Wasm + SIMDはネイティブCと10〜40%以内の差に収束します。


8. 実世界のケーススタディ

Figma

FigmaのレンダリングエンジンはすべてC++で書かれており、Emscriptenを通じてWasmにコンパイルされています。これにより複雑なベクターグラフィック演算がブラウザで60fpsで処理されます。

Google Earth

Google Earth for WebもC++エンジンをWasmにポーティングしています。数GBに及ぶ3D地形レンダリングロジックをブラウザで実行します。

Pyodide: ブラウザでPythonを実行

PyodideはCPythonインタープリター全体をWasmにコンパイルし、ブラウザタブ内でPythonを実行します。

<script src="https://cdn.jsdelivr.net/pyodide/v0.27.0/full/pyodide.js"></script>
<script>
  async function runPython() {
    const pyodide = await loadPyodide()

    // ブラウザでnumpyとpandasをインストールして使用
    await pyodide.loadPackage(['numpy', 'pandas'])

    const result = pyodide.runPython(`
    import numpy as np
    import pandas as pd

    # ブラウザ内でnumpy演算
    arr = np.random.randn(1000, 1000)
    eigenvalues = np.linalg.eigvals(arr[:10, :10])
    float(np.abs(eigenvalues).max())
  `)

    console.log('最大固有値:', result)
  }
  runPython()
</script>

クイズ

Q1. WebAssemblyがJavaScriptより数値演算で高速な性能を発揮する根本的な理由は?

答え: 静的型システムと予測可能なAOT/JITコンパイル

解説: JavaScriptは動的型付け言語であり、ランタイムに型推論、インラインキャッシング、隠しクラスの変換など多くの最適化を行う必要があります。一方Wasmはコンパイル時にすべての型が確定しているため、JITエンジンが即座に最適化された機械語を生成できます。さらにWasmバイトコードは解析オーバーヘッドが非常に低く、SIMDやマルチスレッド命令を明示的に使用できます。

Q2. WASI(WebAssembly System Interface)が必要な理由と提供する抽象化は?

答え: OS依存なしにシステムリソースにアクセスするための標準インターフェース

解説: ブラウザ外で実行されるWasmモジュールはファイルシステム、ネットワーク、環境変数などへのアクセスが必要ですが、Wasm自体はサンドボックスで隔離されています。WASIはPOSIX類似のシステムコールをWasmインターフェースとして標準化し、同一の.wasmバイナリがLinux、Windows、macOS、組み込みシステムなど、どこでも同様に動作するようにします。Dockerコンテナを使わずWasm単体でどこでも実行できる「コンテナの未来」と呼ばれます。

Q3. WebGPUがWebGLよりML推論に適している理由は?

答え: コンピュートシェーダーのファーストクラスサポートと明示的なGPUメモリ管理

解説: WebGLはグラフィックレンダリングパイプライン向けに設計されており、汎用並列演算(GPGPU)が制限的でした。ML推論の核心である行列乗算や畳み込みを行うにはフラグメントシェーダーを迂回路として使う必要がありました。WebGPUはコンピュートシェーダーをファーストクラス機能としてサポートし、ストレージバッファを通じた柔軟なメモリアクセスと優れた非同期実行モデルを提供します。実際にONNX Runtime WebのWebGPUバックエンドはWebGLバックエンドと比べて2〜5倍高速な推論速度を実現しています。

Q4. Cloudflare WorkersのV8 isolateベース実行がLambdaよりコールドスタートが速い理由は?

答え: 既存プロセス内でV8 isolateコンテキストのみを作成し、コンテナやOSの初期化が不要

解説: AWS Lambdaはリクエストごとに新しいコンテナ(または実行環境)をプロビジョニングし、OSブート、ランタイム初期化などを経る必要があります。このプロセスには数百ミリ秒から数秒かかります。Cloudflare Workersは既に稼働しているV8プロセス内でメモリ隔離されたisolateをほぼ瞬時(約1ms)に作成します。既存プロセスのJITコンパイル済みコードを再利用できるため、実質的にコールドスタートはゼロに近いです。

Q5. PyodideがWebAssemblyを使ってPythonをブラウザで実行する仕組みは?

答え: CPythonインタープリター全体をEmscriptenでWasmにコンパイル

解説: PyodideはCPython 3.xのソースコード(Cで記述)をEmscriptenツールチェーンでWebAssemblyバイナリにコンパイルします。ブラウザがこのWasmバイナリをロードすると、完全なPythonインタープリターがブラウザタブ内で実行されます。numpyやscipyなどのC拡張モジュールも同様にWasmにコンパイルされて提供されます。JavaScript-Python間の双方向バインディング(PyProxy、JsProxy)により、PythonオブジェクトをJSから、JSオブジェクトをPythonから直接操作できます。JupyterLiteのような完全なブラウザ内Jupyter環境がこの技術を基盤にしています。


まとめ

WebAssemblyは単なる「高速なJavaScriptの代替」を超え、汎用ランタイムプラットフォームとして進化しています。WASI Component Modelの成熟、WebGPUの普及、エッジプラットフォームによるWasm採用の加速により、2026年現在Wasmは以下の領域で標準技術として定着しています。

  • ブラウザAI推論(ONNX Runtime Web + WebGPU)
  • サーバーレスエッジ関数(Cloudflare Workers、Fastly Compute)
  • IoTおよび組み込み(WasmEdge、WAMR)
  • プラグインシステム(Extism、waPC)

Rustとwasm-packから始まるWasm開発、Cloudflare Workersへのデプロイ、WebGPUを使ったブラウザ内MLモデル実行まで、このガイドがエッジコンピューティングの旅の羅針盤となることを願っています。