



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp








生徒ブラウザ上でリアルタイム画像フィルターを実装したいんですが、JavaScriptだとフレームレートが全然出なくて……



WebAssembly(Wasm)を使えばそれが解決できるんじゃ!JavaScriptより2〜4倍高速に動作するWasmをRustで書いて、JavaScriptから呼び出す仕組みを作れば、ブラウザ上でネイティブアプリに近いパフォーマンスが実現できるぞ。wasm-packを使えば環境構築もシンプルじゃ!
結論:WebAssemblyを使えば、ブラウザ上でJavaScriptの2〜4倍高速な画像処理が実現できます。動画編集ツール・画像フィルター・物理シミュレーション・暗号化処理などJavaScriptだけでは速度の限界を感じる場面で特に有効です。
本記事では、WebAssemblyの概要・Rustとwasm-packによる開発環境構築・画像処理の実装例(グレースケール・セピア・エッジ検出・明るさ調整)・Web WorkersとWasmの組み合わせ・Vite/Reactへの統合・メモリ管理を完全解説します。
WebAssembly(Wasm)は2017年にW3Cで標準化された低レベルバイナリフォーマットです。C/C++・Rust・Goなどで書かれたコードをコンパイルしてブラウザ上で実行できます。Chrome・Firefox・Safari・Edgeのすべての主要ブラウザが対応しており、2026年現在ではほぼ全ブラウザで利用可能です。
WebAssemblyが高速な理由は2点あります。バイナリ形式なのでJavaScriptのようなテキストパースが不要で読み込みが速い点、および静的型付けなのでJITコンパイラの推測が不要で最適化されたコードが生成される点です。特に整数・浮動小数点演算が多い画像処理では顕著な差が出ます。


>【JavaScript vs WebAssembly の比較】
項目 | JavaScript | WebAssembly
-----------------|--------------------|------------------
実行形式 | テキスト(JIT最適化) | バイナリ(事前最適化済み)
速度 | 相対速度 1.0 | 相対速度 2〜4倍
型付け | 動的型付け | 静的型付け
DOM操作 | ✅ 可能 | ❌ 不可(JSを経由)
メモリモデル | ガベージコレクション | 線形メモリ(手動または RAII)
使い所 | UI・ロジック全般 | 計算集約型の処理
バンドルサイズ | ソースのまま | コンパクトなバイナリ(gz済みで10〜100KB程度)
| タスク | JavaScript (ms) | WebAssembly (ms) | 高速化倍率 |
|---|---|---|---|
| 大量配列のソート(100万件) | 120 | 45 | 2.7倍 |
| 画像処理(グレースケール変換 4K画像) | 200 | 70 | 2.9倍 |
| 物理演算シミュレーション | 300 | 100 | 3.0倍 |
| 暗号化処理(SHA-256 10MB) | 180 | 55 | 3.3倍 |
| Sobel エッジ検出(FHD画像) | 450 | 130 | 3.5倍 |
上記ベンチマークはChrome 122、Apple M2チップでの計測値です。デバイスやブラウザによって差はありますが、計算集約型の処理では一貫してWebAssemblyが優位です。
# 1. Rust のインストール
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# 2. wasm-pack のインストール(Rust → WebAssembly コンパイラ)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# 3. wasm-bindgen ターゲットを追加
rustup target add wasm32-unknown-unknown
# バージョン確認
rustc --version # rustc 1.82.0 以上
wasm-pack --version # wasm-pack 0.13 以上
# 4. プロジェクトを作成
cargo new --lib wasm-image-processor
cd wasm-image-processor
# Cargo.toml - 依存関係の設定
[package]
name = "wasm-image-processor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"] # WebAssemblyにコンパイルするには cdylib が必要
[dependencies]
wasm-bindgen = "0.2" # JavaScriptとのバインディング生成
[profile.release]
opt-level = 3 # 最大最適化
lto = true # Link Time Optimization(バイナリサイズ削減)
codegen-units = 1 # コード生成単位を1に(さらなる最適化)
// src/lib.rs
use wasm_bindgen::prelude::*;
/// グレースケール変換
/// Canvas の ImageData(RGBA 配列)を受け取ってグレースケールに変換する
#[wasm_bindgen]
pub fn apply_grayscale(image_data: &mut [u8]) {
for i in (0..image_data.len()).step_by(4) {
let r = image_data[i] as f32;
let g = image_data[i + 1] as f32;
let b = image_data[i + 2] as f32;
// 輝度計算(NTSC係数:人間の目の感度に合わせた重み付け)
let gray = (r * 0.299 + g * 0.587 + b * 0.114) as u8;
image_data[i] = gray;
image_data[i + 1] = gray;
image_data[i + 2] = gray;
// アルファ値 (image_data[i + 3]) はそのまま
}
}
/// セピア変換
#[wasm_bindgen]
pub fn apply_sepia(image_data: &mut [u8]) {
for i in (0..image_data.len()).step_by(4) {
let r = image_data[i] as f32;
let g = image_data[i + 1] as f32;
let b = image_data[i + 2] as f32;
image_data[i] = ((r * 0.393 + g * 0.769 + b * 0.189) as u8).min(255);
image_data[i + 1] = ((r * 0.349 + g * 0.686 + b * 0.168) as u8).min(255);
image_data[i + 2] = ((r * 0.272 + g * 0.534 + b * 0.131) as u8).min(255);
}
}
/// 明るさ調整(factor: 0.0〜2.0、1.0で変化なし)
#[wasm_bindgen]
pub fn adjust_brightness(image_data: &mut [u8], factor: f32) {
for i in (0..image_data.len()).step_by(4) {
image_data[i] = ((image_data[i] as f32 * factor) as u8).min(255);
image_data[i + 1] = ((image_data[i + 1] as f32 * factor) as u8).min(255);
image_data[i + 2] = ((image_data[i + 2] as f32 * factor) as u8).min(255);
}
}
/// コントラスト調整(factor: 0.0〜2.0、1.0で変化なし)
#[wasm_bindgen]
pub fn adjust_contrast(image_data: &mut [u8], factor: f32) {
let intercept = 128.0 * (1.0 - factor);
for i in (0..image_data.len()).step_by(4) {
image_data[i] = ((image_data[i] as f32 * factor + intercept).clamp(0.0, 255.0)) as u8;
image_data[i + 1] = ((image_data[i + 1] as f32 * factor + intercept).clamp(0.0, 255.0)) as u8;
image_data[i + 2] = ((image_data[i + 2] as f32 * factor + intercept).clamp(0.0, 255.0)) as u8;
}
}
/// 色反転(ネガフィルム効果)
#[wasm_bindgen]
pub fn invert_colors(image_data: &mut [u8]) {
for i in (0..image_data.len()).step_by(4) {
image_data[i] = 255 - image_data[i];
image_data[i + 1] = 255 - image_data[i + 1];
image_data[i + 2] = 255 - image_data[i + 2];
}
}
/// Sobel エッジ検出
/// width・height を受け取ってエッジを検出する
#[wasm_bindgen]
pub fn apply_edge_detection(image_data: &[u8], width: u32, height: u32) -> Vec {
let w = width as usize;
let h = height as usize;
let mut output = vec![0u8; image_data.len()];
// まずグレースケール変換
let mut gray = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let idx = (y * w + x) * 4;
let r = image_data[idx] as f32;
let g = image_data[idx + 1] as f32;
let b = image_data[idx + 2] as f32;
gray[y * w + x] = (r * 0.299 + g * 0.587 + b * 0.114) as u8;
}
}
// Sobelフィルター(端のピクセルは除外)
for y in 1..h-1 {
for x in 1..w-1 {
let gx = -(gray[(y-1)*w + (x-1)] as i32)
+ (gray[(y-1)*w + (x+1)] as i32)
- 2*(gray[y*w + (x-1)] as i32)
+ 2*(gray[y*w + (x+1)] as i32)
- (gray[(y+1)*w + (x-1)] as i32)
+ (gray[(y+1)*w + (x+1)] as i32);
let gy = -(gray[(y-1)*w + (x-1)] as i32)
- 2*(gray[(y-1)*w + x] as i32)
- (gray[(y-1)*w + (x+1)] as i32)
+ (gray[(y+1)*w + (x-1)] as i32)
+ 2*(gray[(y+1)*w + x] as i32)
+ (gray[(y+1)*w + (x+1)] as i32);
let magnitude = ((gx*gx + gy*gy) as f32).sqrt().min(255.0) as u8;
let idx = (y * w + x) * 4;
output[idx] = magnitude;
output[idx + 1] = magnitude;
output[idx + 2] = magnitude;
output[idx + 3] = 255;
}
}
output
}
/// ピクセル数を返す(テスト用)
#[wasm_bindgen]
pub fn pixel_count(image_data: &[u8]) -> u32 {
(image_data.len() / 4) as u32
}
# WebAssembly としてビルドする
wasm-pack build --target web
# 開発時は --dev オプションでデバッグ情報を含める
wasm-pack build --target web --dev
# 生成されるファイル
pkg/
├── wasm_image_processor.js # JavaScript バインディング(wasm-bindgenが自動生成)
├── wasm_image_processor_bg.wasm # バイナリ(Rustのコードがコンパイルされたもの)
├── wasm_image_processor.d.ts # TypeScript型定義ファイル(自動生成)
└── package.json # npm公開用(ローカル利用時は不要)
<!-- index.html - Wasm を読み込んで使う -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Wasm 画像フィルター</title>
<style>
body { font-family: sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; }
canvas { max-width: 100%; border: 1px solid #ddd; border-radius: 8px; }
.controls { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; }
button { padding: 8px 16px; border: none; background: #3b82f6; color: white;
border-radius: 6px; cursor: pointer; }
button:hover { background: #2563eb; }
.timing { font-size: 12px; color: #666; }
</style>
</head>
<body>
<h1>WebAssembly 画像フィルター</h1>
<input type="file" id="fileInput" accept="image/*">
<div class="controls">
<button onclick="applyFilter('grayscale')">グレースケール</button>
<button onclick="applyFilter('sepia')">セピア</button>
<button onclick="applyFilter('invert')">色反転</button>
<button onclick="applyFilter('edge')">エッジ検出</button>
<button onclick="applyFilter('brightness')">明るさ +20%</button>
<button onclick="applyFilter('contrast')">コントラスト +30%</button>
<button onclick="resetImage()">リセット</button>
</div>
<p class="timing" id="timing">フィルターを適用するとここに処理時間が表示されます</p>
<canvas id="canvas"></canvas>
<script type="module">
import init, {
apply_grayscale, apply_sepia, apply_edge_detection,
invert_colors, adjust_brightness, adjust_contrast
} from './pkg/wasm_image_processor.js'
// Wasm モジュールを初期化
await init()
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let originalImageData = null
// 画像ファイルの読み込み
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0]
const img = new Image()
img.src = URL.createObjectURL(file)
img.onload = () => {
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
}
})
window.resetImage = function() {
if (originalImageData) ctx.putImageData(originalImageData, 0, 0)
}
window.applyFilter = function(filter) {
if (!originalImageData) return
// 元の画像データのコピーを取得
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const start = performance.now()
switch (filter) {
case 'grayscale':
apply_grayscale(imageData.data)
ctx.putImageData(imageData, 0, 0)
break
case 'sepia':
apply_sepia(imageData.data)
ctx.putImageData(imageData, 0, 0)
break
case 'invert':
invert_colors(imageData.data)
ctx.putImageData(imageData, 0, 0)
break
case 'brightness':
adjust_brightness(imageData.data, 1.2)
ctx.putImageData(imageData, 0, 0)
break
case 'contrast':
adjust_contrast(imageData.data, 1.3)
ctx.putImageData(imageData, 0, 0)
break
case 'edge': {
// エッジ検出は新しい配列を返す
const result = apply_edge_detection(
imageData.data, canvas.width, canvas.height
)
const resultImageData = new ImageData(
new Uint8ClampedArray(result), canvas.width, canvas.height
)
ctx.putImageData(resultImageData, 0, 0)
break
}
}
const elapsed = (performance.now() - start).toFixed(2)
document.getElementById('timing').textContent =
`処理時間: ${elapsed}ms(Wasm)`
}
</script>
</body>
</html>
# Vite + React プロジェクトにWasmを組み込む
npm create vite@latest my-wasm-app --template react-ts
cd my-wasm-app
# Wasm対応プラグインをインストール
npm install vite-plugin-wasm vite-plugin-top-level-await
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'
export default defineConfig({
plugins: [
react(),
wasm(),
topLevelAwait(), // Wasmの初期化にtop-level awaitを使うため
],
})
// src/hooks/useImageProcessor.ts
import { useState, useEffect, useRef, useCallback } from 'react'
type FilterType = 'grayscale' | 'sepia' | 'invert' | 'edge' | 'brightness' | 'contrast'
export function useImageProcessor() {
const [wasmReady, setWasmReady] = useState(false)
const wasmRef = useRef<{
apply_grayscale: (data: Uint8ClampedArray) => void
apply_sepia: (data: Uint8ClampedArray) => void
apply_edge_detection: (data: Uint8ClampedArray, w: number, h: number) => Uint8Array
invert_colors: (data: Uint8ClampedArray) => void
adjust_brightness: (data: Uint8ClampedArray, f: number) => void
adjust_contrast: (data: Uint8ClampedArray, f: number) => void
} | null>(null)
useEffect(() => {
async function loadWasm() {
const wasm = await import('../../pkg/wasm_image_processor.js')
await wasm.default() // Wasmモジュールを初期化
wasmRef.current = wasm
setWasmReady(true)
}
loadWasm()
}, [])
const applyFilter = useCallback((
canvas: HTMLCanvasElement,
filter: FilterType
): number => {
if (!wasmRef.current) return 0
const ctx = canvas.getContext('2d')!
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const wasm = wasmRef.current
const start = performance.now()
switch (filter) {
case 'grayscale': wasm.apply_grayscale(imageData.data); break
case 'sepia': wasm.apply_sepia(imageData.data); break
case 'invert': wasm.invert_colors(imageData.data); break
case 'brightness': wasm.adjust_brightness(imageData.data, 1.2); break
case 'contrast': wasm.adjust_contrast(imageData.data, 1.3); break
case 'edge': {
const result = wasm.apply_edge_detection(imageData.data, canvas.width, canvas.height)
const resultImageData = new ImageData(
new Uint8ClampedArray(result), canvas.width, canvas.height
)
ctx.putImageData(resultImageData, 0, 0)
return performance.now() - start
}
}
ctx.putImageData(imageData, 0, 0)
return performance.now() - start
}, [])
return { wasmReady, applyFilter }
}
WebAssemblyは高速ですが、メインスレッドで実行するとUIをブロックする可能性があります(特に4K以上の大きな画像)。Web WorkerとWebAssemblyを組み合わせることで、UIのフリーズを防ぎながら高速処理を実現できます。
// wasm-worker.js - Wasm を Worker 内で実行する
import init, { apply_grayscale, apply_sepia } from './pkg/wasm_image_processor.js'
// Worker起動時にWasmを初期化
await init()
self.addEventListener('message', async (event) => {
const { buffer, filter, width, height } = event.data
const imageData = new Uint8ClampedArray(buffer)
const start = performance.now()
switch (filter) {
case 'grayscale':
apply_grayscale(imageData)
break
case 'sepia':
apply_sepia(imageData)
break
}
const elapsed = performance.now() - start
// 処理済みバッファをTransferableで返す(コピーなし)
self.postMessage(
{ buffer: imageData.buffer, elapsed },
[imageData.buffer] // 所有権を移転して返す
)
})
// main.ts - Worker経由でWasm処理を行う
const wasmWorker = new Worker(new URL('./wasm-worker.js', import.meta.url), {
type: 'module'
})
async function applyFilterInWorker(canvas: HTMLCanvasElement, filter: string) {
const ctx = canvas.getContext('2d')!
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const buffer = imageData.data.buffer
return new Promise<number>((resolve) => {
wasmWorker.addEventListener('message', (e) => {
const resultData = new Uint8ClampedArray(e.data.buffer)
const resultImageData = new ImageData(resultData, canvas.width, canvas.height)
ctx.putImageData(resultImageData, 0, 0)
resolve(e.data.elapsed)
}, { once: true })
// TransferableでWorkerに送る
wasmWorker.postMessage(
{ buffer, filter, width: canvas.width, height: canvas.height },
[buffer]
)
})
}
自分でRustを書かなくても、既にWebAssemblyに対応した既存ライブラリを活用する方法があります。
| ライブラリ | 用途 | 特徴 |
|---|---|---|
| ffmpeg.wasm | 動画エンコード・変換・サムネイル生成 | C/C++のffmpegをWasm化。ブラウザで動画変換が可能 |
| OpenCV.js | 画像認識・顔検出・特徴点抽出 | OpenCVのWasm版。コンピュータビジョン全般に対応 |
| Sharp(wasm版) | 高速画像リサイズ・変換 | Node.jsの画像ライブラリをブラウザで使える |
| SQLite(sql.js) | ブラウザ上でSQLite | サーバーなしでSQL実行・データ分析 |
| TensorFlow Lite for Web | 機械学習推論 | 軽量モデルをWasmで高速推論 |
// ffmpeg.wasm の使用例:ブラウザで動画をGIFに変換
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'
const ffmpeg = createFFmpeg({ log: true })
async function convertToGif(videoFile: File): Promise<string> {
await ffmpeg.load()
const inputName = 'input.mp4'
const outputName = 'output.gif'
ffmpeg.FS('writeFile', inputName, await fetchFile(videoFile))
// FFmpegコマンドで変換(10fps、480px幅のGIF)
await ffmpeg.run(
'-i', inputName,
'-vf', 'fps=10,scale=480:-1:flags=lanczos',
'-loop', '0',
outputName
)
const data = ffmpeg.FS('readFile', outputName)
const url = URL.createObjectURL(new Blob([data.buffer], { type: 'image/gif' }))
return url
}
WebAssemblyは線形メモリを使います。デフォルトでは初期64KBから最大4GBまで拡張可能です。Rustのwasm-bindgenを使う場合は自動的にメモリ管理が行われるため、通常は意識する必要はありません。
// Wasmのメモリ使用量を監視する
// Wasm インスタンスのメモリ情報を確認
import init, { __wasm } from './pkg/wasm_image_processor.js'
await init()
// WebAssembly.Memory オブジェクトから使用量を確認
const memory = __wasm.memory
console.log(`Wasmメモリ: ${memory.buffer.byteLength / 1024 / 1024}MB`)
// 処理後のメモリ解放(Rustのwasm-bindgenが管理するため通常不要)
// ただし Vec<u8> を返す関数は呼び出し後に自動解放される
| エラー | 原因 | 解決方法 |
|---|---|---|
WebAssembly.instantiate(): ...is not an ArrayBuffer | wasmファイルのMIMEタイプが間違っている | サーバーでapplication/wasmを設定 |
TypeError: wasm.xxx is not a function | wasm-bindgenのバインディングが未生成 | wasm-pack buildを再実行 |
| Wasmが初期化前に呼ばれた | await init()の前に関数を呼んでいる | 必ずawait init()の後に使用する |
| メモリ不足エラー | 大きすぎる画像を一度に処理 | 画像を分割処理するか、Web Workerに移す |
【WebAssembly が特に効果的なユースケース】
1. 画像・動画処理
→ OpenCV.js(画像認識・顔検出)のWasm版
→ ffmpeg.wasm でブラウザ上での動画エンコード・変換
→ リアルタイム動画フィルター(ストリーミング処理)
2. ゲーム開発
→ UnityのWebGLビルド(WebAssembly + WebGL)
→ Unreal EngineのHTML5ビルド
→ 物理エンジン(Bullet Physics のWasm版)
3. 暗号化・セキュリティ
→ Rustで書いた暗号ライブラリをクライアントサイドで高速実行
→ 秘密鍵をサーバーに送らずブラウザ内で処理(e2e暗号化)
→ PasswordのKDFをWasmで高速化
4. データ処理・分析
→ 大量CSVのパース・集計をWasmで高速化
→ SQLiteのWasm版(sqlite3.wasm)でブラウザ上でSQL実行
→ DuckDB-WASM によるブラウザ上のOLAP分析
5. 科学計算・シミュレーション
→ 物理シミュレーション(流体・粒子系・分子動力学)
→ 機械学習推論(TensorFlow Lite for WebAssembly)
→ 音声処理・音楽合成エンジン
いいえ。C/C++(Emscripten)・Go(GOARCH=wasm)・AssemblyScript(TypeScriptライク)・Zigなどでも書けます。RustはメモリセーフでWebAssemblyとのエコシステム(wasm-bindgen・wasm-pack)が充実しているため、新規開発では最もおすすめです。JavaScriptに近い文法でWasmを書きたい場合はAssemblyScriptが入門しやすいです。
計算集約型の処理にのみ有効です。DOM操作・イベント処理・API呼び出しはJavaScriptが担います。WasmとJavaScript間のデータ転送にはオーバーヘッドがあるため、小さなデータの頻繁なやりとりよりも、大きなデータを一括処理するケースに向いています。
Rustで書いた簡単な画像処理ライブラリだと、最適化後(opt-level=3・lto=true)のwasmバイナリは10〜50KB程度になります。gzip圧縮後はさらに小さくなります。ffmpeg.wasmのような大型ライブラリは10MB以上になるため、遅延読み込み(動的importまたはCDN)が推奨されます。
wasm-pack build --devでデバッグ情報を含めてビルドすると、Chrome DevToolsでRustのソースコードにステップインできます(DWARF debuginfoが有効な場合)。また#[wasm_bindgen]付きの関数にweb_sys::console::log_1()を挿入してJavaScriptのコンソールにログを出す方法も有効です。
できます。wasm-pack publishコマンドで生成されたpkg/ディレクトリをnpmに公開できます。--target bundlerオプションでVite/Webpack向けに最適化されたパッケージを生成できます。
wasm-pack build --target web → 生成された.js/.wasmファイルをHTMLやViteから読み込むWebAssemblyは「JavaScriptの限界」を感じた場面で選ぶ技術です。既存のRust/C++ライブラリを活用できる点も大きな強みです。Web WorkersとWasmの組み合わせで、UIのフリーズなく高速処理を実現できます。まず小さなユースケースで試してみましょう。
公式サイト より
今すぐ
無料カウンセリング
を予約!