Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScript で WebAssembly を用いた 型安全なプラグイン設計

TypeScript で WebAssembly を用いた 型安全なプラグイン設計

TSKaigi 2026で話をしました。

https://2026.tskaigi.org/talks/8

Avatar for glassmonenkey

glassmonenkey

May 21, 2026

More Decks by glassmonenkey

Other Decks in Science

Transcript

  1. SPEAKER 永野 峻輔 (@glassmonkey) Ubie 株式会社 / データプロダクトマネージャー 興味関心 データ品質

    / 多言語ランタイム / 型による開発体験の 改善 RECENT TALKS Go Conference 2025「encoding/json/v2 で何が変 わるか」 など PRODUCT https://php-play.dev HOBBIES 個人開発 / ゲーム / 読書 永野 峻輔 / @glassmonkey / #tskaigi_leverages 02 / 46
  2. SIDE PROJECT 宣伝: WebAssembly製 PHP Playground URL https://php-play.dev ブラウザだけで PHP

    5.6 から 8.x までを切り 替えて実行できる個人プロダクト。 気になる人は「PHP Playground」で検索。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 03 / 46
  3. SIDE PROJECT 宣伝: pure Go 製の BigQuery emulator UPSTREAM Go

    goccy / bigquery-emulator BigQuery を pure Go で再実装したエミュレータ FORK MY FORK Go glassmonkey / bigquery-emulator 自分の手で機能拡張しつつメンテ中 UPSTREAM github.com/goccy/bigquery-emulator MY FORK github.com/glassmonkey/bigquery-emulator BigQuery を pure Go で再実装したエミュレータの fork。本家にない挙動を埋めながら開発中。 自チームのテスト環境で導入・実験中 永野 峻輔 / @glassmonkey / #tskaigi_leverages 04 / 46
  4. AGENDA 本日のお品書き 01 WASM の歴史 asm.js から WASI preview 3

    まで、ポータブルなバイナリの現在地 02 Component Model と WIT preview 2 で入った、言語非依存な型安全境界 03 TypeScript × WASM ホスト TS 固定で 4 パターンのゲストを並べて手触りを比較 04 比較検討 条件 / 特徴 / 実験 / 結果 / 考察 05 まとめ 持ち帰ってほしい 3 つのこと 永野 峻輔 / @glassmonkey / #tskaigi_leverages 05 / 46
  5. ABOUT UBIE — 背景 「適切な医療」にたどり着けている人は ごくわずか 100 発症・自覚 何かしらの発症があった人 66

    受診検討 受診を検討した 48 受診 実際に受診した 38 適切な診断 適切な診断に たどり着いた 発症を 100 としたとき、適切な診断にたどり着くのは 38 — 62% のマッチング不全が起きている 出典: Ubie 実施の患者サーベイ (2022/02, N=4,462) 永野 峻輔 / @glassmonkey / #tskaigi_leverages 08 / 46
  6. ABOUT UBIE — インパクト 2020 年提供開始以来、 適切な医療へのアクセスを支援 MONTHLY USERS 1,200万人

    月間利用者数 PARTNER CLINICS 15,000+ 提携医療機関数 TOTAL USES 1.8億回+ 累計利用回数 SYMPTOMS 3,500+ 対応する症状 DISEASES 1,100+ 対応する病名 SATISFACTION 91.1% ユビー利用者の「受診してよかった」 永野 峻輔 / @glassmonkey / #tskaigi_leverages 10 / 46
  7. GOAL TypeScript から WASM を 型安全に呼ぶ 実装の手触りを掴む ✓ 話すこと −

    話さないこと WASM / WASI の現在地 — preview 1 / 2 / 3 の成熟度はどうか? 何のために TS から WASM を呼ぶのか — Component Model + WIT が引 く型安全な境界の価値と制約 採用判断 — 4 パターンの実装比較 + 呼び出し時間 vs 計算時間の体感 WASM バイナリ仕様の詳細 (オペコード・型エンコーディング) 各ランタイム実装の内部 (JIT / コード生成戦略) ブラウザでの利用 (本トークはサーバー / プラグイン文脈) preview 3 の async 仕様の細部 永野 峻輔 / @glassmonkey / #tskaigi_leverages 11 / 46
  8. SECTION 01 01 WASM の歴史 asm.js から WASI preview 3

    まで、ポータブルなバイナリの現 在地 永野 峻輔 / @glassmonkey / #tskaigi_leverages 12 / 46
  9. DEFINITION WebAssembly (WASM) とは ネイティブに近いパフォーマンスで動作する、 コンパクトなバイナリー形式の低レベルなアセンブリー風言語 C / C++ /

    Rust など複数言語のコンパイル先になる もとはブラウザでの高速実行を目指して設計 ── JavaScript と並べて動く のちにブラウザの外でも動くサンドボックス実行環境へと拡張 (WASI) 出典: MDN Web Docs 「WebAssembly 」 永野 峻輔 / @glassmonkey / #tskaigi_leverages 13 / 46
  10. EVOLUTION asm.js から現在まで 2013 asm.js JS サブセットで高速化 2015 WASM 構想発表

    4 ブラウザベンダーが 合意 2017 MVP 出荷 主要ブラウザに搭載 2019 WASI preview 1 ブラウザ外へ進出 2024 WASI preview 2 Component Model 登 場 2026〜 WASI preview 3 async / stream / future (開発中) 永野 峻輔 / @glassmonkey / #tskaigi_leverages 14 / 46
  11. ORIGIN ブラウザの高速化 01 Fast — 高速に実行で きる C / C++

    コンパイラが出すような最適化済み の低レベルコードを、ネイティブに近い速度 でブラウザ内で走らせる。 02 Portable — 同じバイナ リが動く 機種・OS・ブラウザを問わず同じバイナリ が同じ挙動で動く、ハードウェア非依存の実 行形式。 03 Compact — 転送が軽い JS ソースより遥かにコンパクトなバイナリ形 式で、ロード時間と帯域を節約しレスポンス を改善する。 出典: Haas et al.「Bringing the Web up to Speed with WebAssembly」(PLDI 2017) §1.1 Design Goals (Safe / Fast / Portable / Compact) より、ブラウザ 高速化文脈の 3 つを抑粋 永野 峻輔 / @glassmonkey / #tskaigi_leverages 15 / 46
  12. BEYOND THE BROWSER WASI の登場 ブラウザの外で WASM をサンドボックスとして動かすための、 OS リソースへの標準アクセス

    API 代表ランタイム: wasmtime / wasmer / WAMR / wazero ファイル・ネットワークは ホストが許可したものだけ貸し出される 用途: サーバーレス・エッジ・プラグイン基盤 など 永野 峻輔 / @glassmonkey / #tskaigi_leverages 16 / 46
  13. WASI WASI の進化 — preview 1 → 2 → 3

    世代 時期 コンセプト 何が嬉しくなったか preview 1 2019 POSIX 風 syscall + capability ブラウザ外で WASM が動く基盤 preview 2 2024 Component Model + WIT で再定義 高水準な型で言語非依存に喋れる preview 3 開発中 (2026/05) async / stream / future を一級市民化 非同期処理を言語をまたいで扱える preview 1 → 2 は 設計の作り直しに近い大きな変更 / preview 3 は preview 2 を土台にした機能拡張 本トークの「型安全プラグイン」は preview 2 (Component Model) の上に立つ 永野 峻輔 / @glassmonkey / #tskaigi_leverages 17 / 46
  14. SECTION 02 02 preview 1 → preview 2 で 何が変わるか

    従来の書き様から、Component Model が解決したものを見る 永野 峻輔 / @glassmonkey / #tskaigi_leverages 18 / 46
  15. HOW IT'S WRITTEN — preview 1 preview 1 の書き様 ホスト

    TS から WASM の run(data) を呼ぶ — 契約 + 実装 契約 — 手書きの TS interface // .wasm exports は any。型は人間が書く interface PluginExports { // (size) → ptr alloc(size: number): number; // (ptr, len) → resPtr // in: JSON bytes // out: JSON bytes (length-prefixed) run(ptr: number, len: number): number; memory: WebAssembly.Memory; } // ↑ ABI ルールはコメントでしか書けない host.ts (preview 1 / 独自 ABI) // 手書きの interface を import (型保証なし) import type { PluginExports } from "./plugin-types"; const exp = wasm.instance.exports as PluginExports; // 1. 入力を JSON バイト列へ const bytes = new TextEncoder().encode(JSON.stringify(data)); // 2. ゲストのメモリを確保しバイト列を書き込む const ptr = exp.alloc(bytes.length); new Uint8Array(exp.memory.buffer).set(bytes, ptr); // 3. ポインタと長さを渡して実行 const resPtr = exp.run(ptr, bytes.length); // 4. 戻り値を手動で取り出して JSON.parse const out = readJsonAt(exp.memory, resPtr); // → out は any、型ズレは実行時に発覚 永野 峻輔 / @glassmonkey / #tskaigi_leverages 19 / 46
  16. CHARACTERISTICS & ISSUES preview 1 の特徴と課題 特徴 — とにかく動かせる 課題

    — 型と拡張性が弱い 「小さく始める」には十分だが、プラグインを 言語をまたいで型安全に送り受けさせたい複雑な用途で頭打ちに なる ツールチェインが成熟していて、最小限のコードで取り込める シリアライズ規約を自由に選べる (JSON / MessagePack / 生バイト) POSIX 風 syscall で OS リソースにアクセスできる 外部 I/O が貧弱 — POSIX 風の最低限 (file/fd) のみ、HTTP / sockets は標 準に無い C / POSIX 寄りで型表現が低水準 — 生ポインタ中心、linear memory 公開 でサンドボックスが弱体化しやすい 型ズレが実行時まで発覚しない — ホストもゲストも手書きの interface 永野 峻輔 / @glassmonkey / #tskaigi_leverages 20 / 46
  17. HOW IT'S WRITTEN — preview 2 preview 2 の書き様 同じ

    run(data) を、型生成された関数として呼ぶ plugin.wit ( 言語非依存な契約) package my:plugin; interface transform { record input { id: string, payload: list<u8>, } run: func(d: input) -> result<string, string>; } world plugin { export transform; } host.ts ( 自動生成された型を import) import { transform } from "./gen/plugin.js"; // record / list は Canonical ABI が橋渡し const res = transform.run(data); if (res.tag === "ok") { const out: string = res.val; // ← string が保証される } else { console.error(res.val); // err 側も型安全 } // 型ズレはビルド時に tsc が落とす 永野 峻輔 / @glassmonkey / #tskaigi_leverages 21 / 46
  18. CORE CONCEPTS コードの裏側 — Component Model と WIT preview 2

    を支える 2 つの導入。Component Model が型安全な合成規格を与え、WIT がそれを書く言語。 01 · COMPONENT MODEL 型安全な合成規格 WASM コアの上に載る提案。ABI レベルで string / list / record / variant / resource などの高水準な型を扱える。 ポインタやオフセットを意識せずホスト言語のオブジェクトでやりと りできる。 02 · WIT 境界を宣言する IDL Wasm Interface Types。Component Model の型を書くための言語。 interface / record / func を宣言する。 WIT 1 つから TSの .d.ts 、Rustのバインドなどが自動生成される。 WIT で境界を宣言し、Component Model が ABI で守る。型を両側で同期させて言語非依存な型境界を作る。 preview 1 で貧弱だった外部 I/O も、wasi-http のように WIT で標準化され、HTTP request/response を 一級 市民として扱える。 出典: Bytecode Alliance Component Model ドキュメント / WIT 仕様 (component-model.bytecodealliance.org ) 永野 峻輔 / @glassmonkey / #tskaigi_leverages 22 / 46
  19. BEFORE / AFTER — 詳細 preview 2 で何が解決されたか 軸 preview

    1 preview 2 (Component Model + WIT) 型整合の検知 実行時にバリデーション ビルド時にコンパイルエラー データの受け渡し ポインタ + 長さ + 独自シリアライズ record / list / result をそのまま 失敗時の表現 数値 / any 経由の例外 result<T,E> (判別共用体) 多言語拡張 個別に規約を拡張 (破綻しやすい) WIT 1 つで両側自動同期 API の進化 preview 1 → 2 は破壊的変更 preview 2 → 3 はvirtualization で polyfill 可能 永野 峻輔 / @glassmonkey / #tskaigi_leverages 23 / 46
  20. BEFORE / AFTER — 結論 人間がポインタを扱うコードから、 言語仕様が型安全に吸収する世界へ BEFORE — preview

    1 手書きの境界線 ホスト/ゲスト両側で interface を写経し、ポイ ンタとシリアライズを自前で運ぶ AFTER — preview 2 型で守られた境界線 WIT 1 つから両側の型が自動生成。ズレはビルド時 に落ちる 出典: Bytecode Alliance「WASI 0.2 Launched」(2024.01.25) — Composition がもたらした転換 永野 峻輔 / @glassmonkey / #tskaigi_leverages 24 / 46
  21. RUNTIMES preview 2 を取り巻く環境 他のランタイムは preview 1 を完全対応、preview 2 /

    Component Model は追従中 preview 2 production ready wasmtime Component Model / WIT の参照実装 preview 3 の async / stream も先行実 装 01 preview 1 のみ (独 自拡張 WASIX) Wasmer 独自拡張 WASIX に投資。 Component Model は別路線 で追従中。 02 preview 1 のみ WasmEdge クラウド / エッジ / AI 推論向 け。Component Model は experimental 段階。 03 preview 1 のみ wazero / WAMR Go 組み込み / 組込み向けの 軽量実装。preview 2 は実験 段階。 結論 — Component Model + WIT を本番ホストで動かすなら、現実的な選択肢は実質 wasmtime 一択。本トークも @bytecodealliance/jco 経由 で wasmtime 系の世界観を前提にしている。 出典: wasmruntime.com「WASI Preview 2 vs WASIX」(2026.01) / Bytecode Alliance Roadmap / wasmtime v25 release notes 永野 峻輔 / @glassmonkey / #tskaigi_leverages 25 / 46
  22. SECTION 03 03 TypeScript から WASM を呼ぶ preview 1 と

    preview 2 を、TS 開発者目線の実装で比べる 永野 峻輔 / @glassmonkey / #tskaigi_leverages 26 / 46
  23. TOOLING jco — TS / JS 側の WASM ツールチェイン Bytecode

    Alliance 提供。Component Model / WASI Preview 2 を JS から扱うための公式 ツール CORE jco transpile .wasm Component → ES Module + .d.ts を生成。 TS 側はこの .d.ts を import するだけで型安全 に呼べる。preview 2 のホストコードはこれ が土台。 GUEST jco componentize .ts / .js → .wasm Component に変換 (SpiderMonkey 同梱)。 ゲスト側を TS で書く道。Tech Preview だ が、TS ロジックを他ランタイムへ携行でき る。 RUNNER jco run / serve Node 上で WASI Command / HTTP Component を起動するランナー。 動作確認やローカル検証に。本トークでは扱 わないが知っておくと便利。 他言語の wit-bindgen に対応する JS 向け bindings ジェネレータ。 本資料の preview 2 セクションのコードは、すべて jco transpile 経由で生成された .d.ts を import している。 出典: github.com/bytecodealliance/jco / npm: @bytecodealliance/jco 永野 峻輔 / @glassmonkey / #tskaigi_leverages 27 / 46
  24. CALLING preview 1 — FROM TS preview 1 を TS

    から呼ぶ host.ts // 1. 手書きの interface (型保証なし) interface PluginExports { memory: WebAssembly.Memory; alloc(n: number): number; run(ptr: number, len: number): number; } const { instance } = await WebAssembly.instantiate(bytes, imports); const exp = instance.exports as unknown as PluginExports; // 2. ゲストに渡すために JSON シリアライズ → 共有メモリにコピー const buf = new TextEncoder().encode(JSON.stringify(input)); const ptr = exp.alloc(buf.length); new Uint8Array(exp.memory.buffer).set(buf, ptr); // 3. ポインタと長さを手で受け渡し → 戻り値も手動デコード const resPtr = exp.run(ptr, buf.length); const out = readJsonAt(exp.memory, resPtr); // 型は any、ズレは実行時に 境界の規約・メモリ管理・シリアライズを、すべて TS 側で自前実装する 永野 峻輔 / @glassmonkey / #tskaigi_leverages 28 / 46
  25. CHARACTERISTICS — preview 1 preview 1 の特徴 ① interface の手動運用

    ホスト側 interface はゲストの実体と無 関係に書ける。人間が両側で同期させる必 要がある。 ② 冗長な手続き alloc → write → run → read + シリ アライズが呼び出しの度に走る。SDK に隠 蔽すれば一定カバーできるが薄いラッパー が必須。 ③ 型情報の欠落 exports は any 、戻り値も any 同然。型 ズレはランタイムまで気づけない。 境界のルールを TS 側で全部明文化していない → エディタ・型システムの支援が効かない 永野 峻輔 / @glassmonkey / #tskaigi_leverages 29 / 46
  26. CALLING preview 2 — FROM TS preview 2 を TS

    から呼ぶ plugin.wit package my:plugin; interface transform { record input { id: string, payload: list<u8>, } run: func(d: input) -> result<string, string>; } world plugin { export transform; } host.ts // WIT から自動生成された .d.ts を import import { transform } from "./gen/plugin.js"; // オブジェクトのまま渡せる (Canonical ABI が橋渡し) const res = transform.run({ id: "tx-1", payload: bytes }); if (res.tag === "ok") { const out: string = res.val; // out は string と保証される } else { console.error(res.val); // err も型安全 } // 型ズレはビルド時に tsc が落とす WIT 1 つで .d.ts も Rust バインドも自動生成 — alloc / ptr / JSON.parse は不要 永野 峻輔 / @glassmonkey / #tskaigi_leverages 30 / 46
  27. WALKTHROUGH — preview 2 preview 1 との比較 — 境界がどう変わっ たか

    ① interface の出どころ preview 1: ホストが interface を手書き (ズレに気付けない) preview 2:WIT 由来の .d.ts を両側で自 動生成 ② メモリの運び方 preview 1: 人間が alloc / ptr / len を運 ぶ preview 2:Canonical ABI の内側に隠れ、 record をそのまま渡せる ③ 失敗の表現 preview 1: 数値 / any 経由の例外で握り潰 される preview 2:result<T,E> = TS の判別共 用体で res.tag 分岐 人間が境界を写経する仕事が消え、tsc がビルド時にズレを落とす状態へ 永野 峻輔 / @glassmonkey / #tskaigi_leverages 31 / 46
  28. WIT → TS TYPE MAPPING ① — PRIMITIVES 基本型 —

    number と bigint で受ける WIT は signed/unsigned/幅まで明記するが、TS では number / bigint / boolean / string の 4 種類に集約される WIT 型 TS 型 備考 bool boolean — s8/16/32, u8/16/32 number signed/unsigned は型に出 ない s64, u64 bigint safe integer 範囲を超える f32, f64 number f32 は精度落ち char string 1 Unicode scalar value string string UTF-8 guest.d.ts (jco transpile) export interface Primitives { echoBool(v: boolean): boolean; echoS32(v: number): number; echoU64(v: bigint): bigint; // ← number でなく bigint echoF32(v: number): number; // 3.14 → 3.1400001049... echoChar(v: string): string; } 永野 峻輔 / @glassmonkey / #tskaigi_leverages 32 / 46
  29. WIT → TS TYPE MAPPING ② — COLLECTIONS list<T> は

    TypedArray に 数値の list<T> は幅ごとに専用の TypedArray にマップされる。number[] を渡すと 型エラーになるので、最初から TypedArray で持つか境界で new Uint32Array(arr) に変換するレイヤーを挟む。 WIT 型 TS 型 備考 list<u8> Uint8Array バイト列の定番 list<u16> / <u32> Uint16/32Array number[] では な い list<s32> Int32Array 同上 list<u64> BigUint64Array 要素も bigint list<string> Array<string> 非数値は通常の配 列 guest.d.ts & 呼び出し側 echoListU8(v: Uint8Array): Uint8Array; echoListU32(v: Uint32Array): Uint32Array; echoListStr(v: Array<string>): Array<string>; // ✗ number[] を渡そうとすると型エラー echoListU32([1, 2, 3]); // ✓ TypedArray で渡す echoListU32(new Uint32Array([1, 2, 3])); 永野 峻輔 / @glassmonkey / #tskaigi_leverages 33 / 46
  30. WIT → TS TYPE MAPPING ③ — GENERICS option /

    result / tuple option は union、tuple はそのまま、result は Rust 側の Err(..) が JS の throw に化けるので try / catch 前提の API になる (Result<T,E> 型も別途 export) WIT 型 TS 型 備考 option<T> T | undefined Option<T> も別途 export result<T, E> return T / throw E err は例外として飛 ぶ tuple<T1, T2> [T1, T2] そのまま配列リテ ラル guest.d.ts & 呼び出し側 // option<string> → string | undefined findUser(id: number): string | undefined; // result<string, error-kind> → T を直接返す parse(s: string): string; try { const v = parse(input); } catch (e) { /* err はここに throw */ } // tuple<u32, string> → [number, string] pair(): [number, string]; 永野 峻輔 / @glassmonkey / #tskaigi_leverages 34 / 46
  31. WIT → TS TYPE MAPPING ④ — USER-DEFINED record /

    variant / enum / flags 命名は kebab-case → camelCase に自動変換。variant は switch (x.tag) で網 羅性検査が効く、flags は全て optionalなので !flags.admin 判定は要注意。 WIT 型 TS 型 備考 record { f-name: T } { fName: T } kebab → camel 自動変換 enum { a, b, c } 'a' | 'b' | 'c' string literal union variant { x(T), y, ... } { tag, val? } の union 判別共用体 flags { a, b, c } { a?, b?, c? } 全 field が optional guest.d.ts (jco transpile) // record { user-id: u32, full-name: string } type UserProfile = { userId: number; fullName: string }; // camelC // variant { ok(string), pending, fail(error-kind) } type Status = | { tag: 'ok', val: string } | { tag: 'pending' } | { tag: 'fail', val: ErrorKind }; // flags { read, write, admin } — 全部 optional type Permissions = { read?: boolean; write?: boolean; admin?: bo 永野 峻輔 / @glassmonkey / #tskaigi_leverages 35 / 46
  32. SIDE NOTE 余談: ゲスト側も TS で書ける jco componentize で TS

    / JS を WASM Component に guest.ts → plugin.wasm // ゲストロジックも TS で書ける import type { Input } from "./gen/types"; export const transform = { run(d: Input) { if (d.payload.length === 0) return { tag: "err", val: "empty" }; return { tag: "ok", val: `processed:${d.id}` }; }, }; // $ jco componentize guest.ts --wit plugin.wit --out plugin.wasm TS 一本でホスト/ゲスト両方を実装できる代わりに、SpiderMonkey 同梱でバイナリは大きめ (起動コストも増える) 永野 峻輔 / @glassmonkey / #tskaigi_leverages 36 / 46
  33. CONDITIONS 比較する 4 パターン — ホスト TS は固定 BASELINE 純

    TS WASM 不使用。ホスト TS の中 で ajv や RegExp を直接呼ぶ。 比較のベースライン。 LEGACY Rust × preview 1 wasi_snapshot_preview1 + 手書きの独自 ABI。length- prefixed binary をやり取りする 旧来の書き方。 ★ FOCUS Rust × preview 2 WIT + Component Model。 cargo-component で型安全な 境界を自動生成。本トークの中 心。 PORTABLE TS × ComponentizeJS TS で書いたゲストを jco componentize で .wasm 化。 SpiderMonkey 同梱で動く Tech Preview。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 38 / 46
  34. CHARACTERISTICS 各条件の特徴 観点 純 TS Rust × preview 1 Rust

    × preview 2 TS × ComponentizeJS 規約 / ABI — wasi_snapshot_preview1 + 独 自 ABI WIT + Component Model WIT + Component Model 型整合の検知 TS 型 実行時バリデーション ビルド時 ビルド時 失敗時の表現 Error / throw 数値 / any 経由の例外 result<T,E> result<T,E> 多言語拡張 不可 (TS の み) 個別に規約を拡張 (破綻しやすい) WIT 1 つで両側自動同期 WIT 1 つで両側自動同期 バイナリ / 起動 JS module のみ 軽い (Rust / 35 KB〜) 軽い (Rust / 14 KB〜) 大きめ (~15 MB / SpiderMonkey 同梱) 永野 峻輔 / @glassmonkey / #tskaigi_leverages 39 / 46
  35. EXPERIMENTS ① 実験シナリオ① — 呼び出し時間と起動時間 "WASM を使うこと自体" のコストを切り分けて測る 2 つの実験。

    hot loop / μs per call 呼び出し時間 — 1 回あたりの時間計測 INPUT 24 byte record ({ id, age } ) を 1 件 GUEST identity = noop(req) → req (ゲスト内処理ゼロ) LOOP 100,000 回呼び出し (戻り値を xor 集約) MEASURE JS↔WASM を 1 回跨ぐ呼び出しコスト (μs / call) cold start / ms 起動時間 — cold start の時間計測 SETUP 毎回子プロセスを spawn し、内側で計測 STAGES .wasm read → instantiate → 初回 noop() 完了 EXCLUDE Node 起動コスト / jco transpile (事前成果物) MEASURE 20 セット median + p95 (バラツキも見る) 入力は事前生成 + ループ巡回で JIT 最適化による call 消失を防ぐ。performance.now() で計測。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 40 / 46
  36. EXPERIMENTS ② 実験シナリオ② — 実ワークロード 2 種 同じアルゴリズムを各系統で別実装し、結果値の一致を sanity check

    してから時間を測る。 regex / ms per 10k 正規表現マッチング時間 — 10k 件の時間計測 INPUT 10,000 件の文字列 (80% match / 20% non-match) PATTERN ^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}$ ENGINE plain-ts: V8 Irregexp / Rust: regex crate VERIFY 全系統で count = 8007 一致 heavy compute / up to 256M ops 重計算時間 — rotate-xor の処理時間計測 INPUT 256 byte payload × 100 件 COMPUTE rotate-xor を N 段 × 累積 u32 ROUNDS = 1k / 3k / 5k / 10k (最大 256M ops ) OBSERVE ops 数によるクロスオーバー (軽 → 重) VERIFY 全系統で累積 u32 値が一致 ※ ウォームアップあり / 5 セット中央値報告。MacBook Air M2 / Node 20.18 / Rust 1.92 / jco 1.19。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 41 / 46
  37. EXPERIMENT RESULTS 実験結果 — 4 パターン × 4 シナリオ MacBook

    Air M2 / Node 20.18 / Rust 1.92 / jco 1.19 / 2026-05-22 計測。中央値報告。 系統 呼び出し時間 μs / call 起動時間 ms (cold start) 正規表現時間 ms / 10k (vs plain-ts) 重計算時間 ms (256M ops, vs plain-ts) 純 TS (plain-ts / V8) 0.003 0.46 0.39 (1.00x) 316.76 (1.00x) Rust × preview 1 ( 独自 ABI) 0.307 1.38 1.50 (3.8x slower) 297.90 (0.94x ⤵) Rust × preview 2 (WIT) 2.704 2.93 3.97 (10.1x slower) 298.38(0.94x ⤵) TS × ComponentizeJS (jco) 45.863 53.43 34.04 (86x slower) 63,208 (200x slower) ※ TS × ComponentizeJS は Tech Preview。jco 1.19 / componentize-js 0.18.5 時点のスナップショット。重計算は ROUNDS=10000 (256M ops) の値。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 42 / 46
  38. READING THE NUMBERS パフォーマンスを目的にすると 期待が外 れる LIGHT 軽処理は plain-ts が圧勝

    正規表現時間は 10k 件 0.39 ms (25.4M rps)。V8 内蔵の Irregexp / TurboFan に Rust native でも追いつかない。 BOUNDARY 呼び出し時間は必ず乗る preview1 で ~300 ns 、preview2 (WIT marshaling 込み) で ~2.7 μs 。高頻度呼 び出しではこれが積み上がる。 HEAVY 重計算時間でも +6% 256M ops の rotate-xor で preview2 は plain-ts の 0.94x = 約 6% だけ速い。 「劇 的な逆転」ではなく「JIT warmup 後の native アドバンテージ」 。 じゃあなぜ WASM か — 型安全な多言語拡張 / 既存資産の取り込み / セキュアなサンドボックス。性能は副次効 果。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 43 / 46
  39. DECISION AXES 採用判断軸 — 採用要件で切る 軽い処理 純 TS で済ます —

    呼び出し時間 > 計算時間なら V8 内蔵エンジンが最速 本番運用 / 多ランタ イム互換 Rust × preview 1 — wasmtime / WasmEdge / WAMR / Wasmer どこでも動く。現状プロダクション採用の現実解 型安全な多言語拡 張 Rust × preview 2 (WIT) — 型と性能の両取り。ただし ホスト側はほぼ wasmtime 一択 TS ロジックの携行 TS × ComponentizeJS — TS 資産を他 wasm ランタイム / ブラウザへそのまま (cold start 許容前提) 本番適用は preview 1 で十分 — preview 2 は wasmtime 以外での本番事例がまだ薄く、型安全とホスト選択のトレードオフ。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 44 / 46
  40. TAKEAWAYS まとめ 01 現在地 — preview 1 は枯れている / preview

    2 はこれから preview 1 はプロダクションで現実に使える成熟度。preview 2 はエコシステムがまだ若く、本資料を作るあいだにも周辺 OSS へい くつか PR を投げた。 「使える」より「育てる」段階。 02 本命は 他言語資産の取り込み — 型安全とランタイムのトレードオフ Rust などの既存ロジックを TS から呼ぶ手段。型安全 (WIT) を取るなら preview 2 + wasmtime にホストが縛られる。型を諦めれば preview 1 で wasmtime / wazero / WasmEdge / Wasmer など多くのランタイムで動く。 03 採用判断 — 無理に WASM を使う必要はない、V8 が強い 正規表現も重計算も plain-ts が速い、もしくは僅差。性能だけを目的に WASM を引き込むと期待が外れる。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 46 / 46
  41. SPONSOR BOOTH 宣伝1: Ubie ブースに遊びに来てください HANG OUT WITH US Ubie

    の TypeScript 事情、 ざっくばらんに話します Ubie のエンジニアが 複数名常駐 しています 本日の発表内容の 深掘り Q&A も大歓迎 ノベルティもあります 🎁 Ubie BOOTH @ TSKaigi 2026 永野 峻輔 / @glassmonkey / #tskaigi_leverages 47 / 46
  42. HIRING 宣伝2: Ubie はエンジニアを募集中です テクノロジーで人々を 適切な医療 に案内する仲間を募集 PRODUCT プロダクトエンジニア バックエンド中心に、プロダクトの価値を作るエンジニア

    PLATFORM プロダクト基盤エンジニア Webフロントエンド / バックエンド / 認証認可基盤 OTHERS SRE・セキュリティ・QA・AI・データ ほか プラットフォーム・品質・データ領域も募集中 INTERN 学生インターン コーポレートAIエンジニア(インターン)ほか 全職種は recruit.ubie.life へ。 まず話だけ聞いてみたい人は、ブースか @glassmonkey までどうぞ。 永野 峻輔 / @glassmonkey / #tskaigi_leverages 48 / 46