Advanced (更新: 2026/6/2)

Claude CodeでWebAssemblyをViteに統合する実践ガイド

Claude CodeでRust製WebAssemblyをViteへ組み込む手順。画像処理、CSV計算、落とし穴、レビュー指示まで解説。

Claude CodeでWebAssemblyをViteに統合する実践ガイド

Claude CodeとWebAssemblyで何を速くするのか

WebAssembly、略してWasmは、RustやC++などで書いた処理をブラウザやNode.js上で高速に動かすための実行形式です。JavaScriptの代わりではなく、画像処理、圧縮、暗号系のバイト列処理、CSV集計のようなCPU負荷が高い小さな塊だけを任せる道具、と考えると初心者でも失敗しにくくなります。

Claude Codeを使う価値は、Wasmそのものを魔法のように生成してもらうことではありません。Rust側の関数、wasm-packのビルド、Viteからの非同期読み込み、TypeScriptの薄いラッパー、レビュー観点、ベンチ手順を同じ文脈でそろえられることです。ここが曖昧なまま「Wasmで速くして」と依頼すると、初期化漏れや境界コストでかえって遅くなるコードが出やすくなります。

この記事では、初心者がそのまま手元で試せる最小構成を作ります。題材は、画像のRGBA反転、CSVの数値列合計、バイト列の軽量チェックサムです。実務では、ブラウザ内の高速処理、既存Rust/C++資産の移植、圧縮や独自バイナリ形式の変換、重い計算をUIスレッドから切り離す設計に広げられます。関連する速度改善の全体像はClaude Codeパフォーマンス最適化も合わせて確認してください。

公式情報は、Wasmの基本をMDN WebAssembly、RustとJavaScriptの橋渡しをwasm-bindgen Guide、ビルドフローをwasm-pack repositoryで確認できます。記事内のコードは学習用に小さくしていますが、構成は実務でレビューしやすい形に寄せています。

使いどころを先に決める

Wasmは「入れれば必ず速い」技術ではありません。JavaScriptとWasmの間を行き来するたびにデータ変換やコピーのコストが出ます。そのため、1ピクセルずつ何度もWasm関数を呼ぶより、画像1枚分の配列を一度渡してまとめて処理するほうが向いています。Claude Codeへ頼む前に、どの処理をWasmへ寄せ、どの処理をJavaScriptに残すかを言語化しておくと差分が安定します。

ユースケースWasmに向く理由Claude Codeに頼む観点
画像処理RGBA配列をまとめて処理でき、Canvasとの相性がよいメモリコピー回数、Canvas更新、ベンチを確認する
暗号、圧縮、独自コーデックバイト列処理が中心で既存Rust資産を流用しやすい監査済みライブラリか、自作してよい範囲かを分ける
CSV、数値計算行列、集計、特徴量作成などループが多い入力サイズ、NaN処理、エラー時の戻り値を決める
既存Rust/C++資産の移植ロジックを再利用でき、ブラウザ配布に向くOS依存API、ファイルI/O、スレッド依存を洗い出す
ブラウザ内高速処理サーバーに送れないデータをローカルで処理できる個人情報、初回ロード、端末性能を見積もる

Masaの検証では、最初から大きな画像エディタを作ろうとするより、1関数だけWasm化してベンチを取るほうが理解が進みました。特に画像処理では「Wasm関数は速いが、Canvasから配列を取り出して戻す部分が重い」という結果になりやすいです。Claude Codeには実装だけでなく、「本当にWasm化する価値があるか」をレビューさせるのが現実的です。

Rustとwasm-packの最小実装

wasm-packは、RustをWasm向けにビルドし、JavaScriptから呼ぶための接着コードとTypeScript定義を生成するCLIです。wasm-bindgenは、Rustの関数や型をJavaScript側へ公開するための橋渡しライブラリです。まずは次の構成で、余計な依存を増やさずに動く関数を3つ作ります。

# Cargo.toml
[package]
name = "wasm-lab"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn invert_rgba(pixels: &mut [u8]) {
    for chunk in pixels.chunks_exact_mut(4) {
        chunk[0] = 255 - chunk[0];
        chunk[1] = 255 - chunk[1];
        chunk[2] = 255 - chunk[2];
    }
}

#[wasm_bindgen]
pub fn sum_csv_column(csv: &str, column: usize) -> f64 {
    csv.lines()
        .filter(|line| !line.trim().is_empty())
        .filter_map(|line| line.split(',').nth(column))
        .filter_map(|cell| cell.trim().parse::<f64>().ok())
        .sum()
}

#[wasm_bindgen]
pub fn fnv1a32(bytes: &[u8]) -> u32 {
    let mut hash = 0x811c9dc5u32;

    for byte in bytes {
        hash ^= u32::from(*byte);
        hash = hash.wrapping_mul(0x01000193);
    }

    hash
}
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
wasm-pack build --target web --out-dir pkg

この例のfnv1a32は、暗号学的に安全なハッシュではありません。認証、署名、パスワード、決済に関わる暗号処理では、Web Crypto APIや監査済みライブラリを優先してください。ここでは「バイト列をまとめてWasmに渡すとどうなるか」を見るための軽量な題材として扱います。

Viteで読み込み、型付きラッパーを作る

wasm-pack build --target webを使うと、pkg/wasm_lab.jspkg/wasm_lab.d.tsが生成されます。Vite側では、最初にinit()を待ってから公開関数を呼びます。ここで大切なのは、初期化を毎回走らせないこと、Wasm関数をUIコンポーネントから直接呼び散らかさないことです。

// src/wasm-client.ts
import init, {
  fnv1a32,
  invert_rgba,
  sum_csv_column,
} from "../pkg/wasm_lab";

export type WasmClient = {
  invertImage(imageData: ImageData): Promise<ImageData>;
  sumCsvColumn(csv: string, columnIndex: number): Promise<number>;
  checksum(bytes: Uint8Array): Promise<number>;
};

let initPromise: Promise<void> | undefined;

async function ensureWasm(): Promise<void> {
  initPromise ??= init().then(() => undefined);
  return initPromise;
}

export const wasmClient: WasmClient = {
  async invertImage(imageData) {
    await ensureWasm();

    const pixels = new Uint8Array(
      imageData.data.buffer,
      imageData.data.byteOffset,
      imageData.data.byteLength,
    );

    invert_rgba(pixels);
    return imageData;
  },

  async sumCsvColumn(csv, columnIndex) {
    await ensureWasm();
    return sum_csv_column(csv, columnIndex);
  },

  async checksum(bytes) {
    await ensureWasm();
    return fnv1a32(bytes);
  },
};
// src/main.ts
import { wasmClient } from "./wasm-client";

const fileInput = document.querySelector<HTMLInputElement>("#csv-file");
const output = document.querySelector<HTMLPreElement>("#output");

fileInput?.addEventListener("change", async () => {
  const file = fileInput.files?.[0];
  if (!file || !output) return;

  const csv = await file.text();
  const total = await wasmClient.sumCsvColumn(csv, 2);
  output.textContent = `column 2 total: ${total.toFixed(2)}`;
});

Viteの設定は、まず標準構成で十分です。直接.wasmをimportする特殊な構成にする場合だけ、Wasm用プラグインやトップレベルawaitの扱いを検討します。初心者は先にwasm-pack生成物をそのまま読み、init()の成功、型定義、ビルド成果物のサイズを確認するほうが安全です。

Claude Codeに渡すレビュー指示

Claude Codeへ「Wasm対応して」とだけ頼むと、動くが遅い、初期化が重複する、例外がUIに出ない、という差分になりがちです。以下のように、対象ファイル、禁止事項、確認コマンド、レビュー観点をまとめて渡します。

Review only these files:
- src/lib.rs
- pkg/wasm_lab.d.ts
- src/wasm-client.ts
- src/main.ts
- src/bench.ts

Goal:
Integrate the Rust WebAssembly module into the Vite app without changing UI behavior.

Check:
1. init() is awaited before any exported Wasm function is called.
2. init() is cached and not repeated for every click or file upload.
3. Large arrays cross the JS-Wasm boundary at most once per user action.
4. DOM updates stay in TypeScript, not inside Rust.
5. The wrapper exposes typed methods and keeps generated pkg files out of hand edits.
6. Benchmarks compare the same input data for JavaScript and Wasm.

Run:
wasm-pack build --target web --out-dir pkg
npm run typecheck
npm run build

この指示は、Claude Codeを実装担当とレビュアーの両方に使うためのものです。最初のターンで実装、次のターンで「境界コスト、非同期初期化、メモリコピー、バンドルサイズだけを批判的に見て」と依頼すると、修正の質が上がります。

簡易ベンチとテスト手順

Wasm化の判断は、体感ではなく同じ入力で測ります。次のベンチは、RGBA配列の反転をJavaScriptとWasmで比べるだけの小さなものです。実務では端末差、入力サイズ、ウォームアップ、開発ビルドと本番ビルドの違いも見ます。

// src/bench.ts
import { wasmClient } from "./wasm-client";

function invertJs(pixels: Uint8Array): void {
  for (let index = 0; index < pixels.length; index += 4) {
    pixels[index] = 255 - pixels[index];
    pixels[index + 1] = 255 - pixels[index + 1];
    pixels[index + 2] = 255 - pixels[index + 2];
  }
}

function cloneImageData(source: Uint8Array, width: number, height: number): ImageData {
  return new ImageData(new Uint8ClampedArray(source), width, height);
}

export async function runBench(): Promise<void> {
  const width = 1920;
  const height = 1080;
  const source = new Uint8Array(width * height * 4);
  crypto.getRandomValues(source);

  const jsPixels = new Uint8Array(source);
  const wasmImage = cloneImageData(source, width, height);

  const jsStart = performance.now();
  invertJs(jsPixels);
  const jsMs = performance.now() - jsStart;

  const wasmStart = performance.now();
  await wasmClient.invertImage(wasmImage);
  const wasmMs = performance.now() - wasmStart;

  console.table({
    javascriptMs: Number(jsMs.toFixed(2)),
    wasmMs: Number(wasmMs.toFixed(2)),
    ratio: Number((jsMs / wasmMs).toFixed(2)),
  });
}
wasm-pack build --target web --out-dir pkg
npm run typecheck
npm run build
npm run dev

数値が期待より伸びないときは、Wasm関数そのものではなく、配列コピー、Canvas読み書き、圧縮前後のデータ変換、ブラウザの開発モードを疑います。Claude Codeにはベンチ結果を貼り、「この差ならWasm化を続けるべきか、JavaScriptのまま最適化すべきか」を聞くと判断材料が増えます。

落とし穴と回避策

1つ目の落とし穴は、初期化が非同期であることです。init()を待たずに関数を呼ぶと、環境によってだけ落ちる不安定なバグになります。ラッパーでensureWasm()を作り、アプリの入口か初回操作時に必ず待つ設計にします。

2つ目は、バンドル肥大です。Rust側で便利そうなcrateを足しすぎると、.wasmと接着コードが一気に大きくなります。画像処理だけなら小さな関数から始め、必要になってからwasm-optや機能フラグを検討します。

3つ目は、JS-Wasm境界コストです。境界とは、JavaScriptとWasmの間で引数や戻り値を受け渡す境目のことです。小さな関数を数万回呼ぶ構成では、計算より呼び出しのほうが重くなることがあります。大きめの配列を一度渡してまとめて処理します。

4つ目は、WasmからDOMを直接触ろうとすることです。ブラウザ画面の更新、イベント、アクセシビリティ属性はTypeScript側に残し、Rust側は純粋な計算に寄せるほうがテストしやすくなります。

5つ目は、メモリコピーの見落としです。ImageDataUint8Array、文字列、CSV本文を渡すたびにコピーが発生する場合があります。ベンチでは、計算時間だけでなく「渡す前後の変換」も含めて測ります。

6つ目は、ブラウザ互換とセキュリティヘッダーです。通常のWasmは主要ブラウザで使えますが、Wasm threadsやSharedArrayBufferを使う場合はCOOPとCOEPによるcross-origin isolationが必要になります。CDN、広告タグ、外部iframeがあるサイトでは、この設定が壊れやすいので早めに確認します。

相談導線と運用の形

Claude Code LabでWasm導入を相談するときは、「速くしたい処理」だけでなく、入力サイズ、対象ブラウザ、既存Rust/C++資産の有無、初回ロードの許容値、失敗時のフォールバックを共有してください。画像処理ならサンプル画像、CSVなら実データに近い匿名ファイル、圧縮や暗号系なら監査要件もあると、レビューの精度が上がります。

個人で試すならこの記事の最小コードで十分です。チームで導入するなら、CLAUDE.mdに「Wasmは計算だけ」「DOMはTypeScript」「pkg生成物は手編集しない」「ベンチなしにWasm化しない」と書いておくと、Claude Codeの出力が安定します。導入ルール、レビュー観点、トレーニングまで整えたい場合は、Claude Code研修・相談で実リポジトリ前提の進め方を相談できます。

検証メモ

この記事で紹介した内容を実際に試した結果、最初につまずいたのはRustの関数ではなく、Vite側でinit()を待つ位置でした。main.tsから直接Wasm関数を呼ぶより、wasm-client.tsに初期化キャッシュを閉じ込めるほうが、画像処理、CSV集計、チェックサムの3パターンを同じ手順で試せます。ベンチでは小さな入力だとJavaScriptのままでも十分速く、Full HD相当の配列や複数列の集計で差が出やすい印象でした。Wasm化は最後の飾りではなく、境界コストまで含めて測ってから採用するのが安全です。

#Claude Code #WebAssembly #Wasm #Rust #performance
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。