Vite+React+TypeScript+SWCでWebAssembly開発環境を試す
はじめに
こんにちは、kackyと申します。私はWebエンジニアとしてテックリードを務めております。今回は社内のフロントエンド技術を紹介し、WebAssembly(WASM)とWebWorkerを利用するチュートリアルを共有します。
フロントエンドの構築
今回は最新のVite、React、TypeScript、SWCの組み合わせを採用しています。このスタックにより、フロントエンド開発が大幅に効率化されます。まずは以下のコマンドでプロジェクトを始めます。
npm create vite@latest react-wasm-webworker --template react-swc-ts
RustでのWASM開発
WASMの開発にはRustを使用します。RustはWASMの実装とパッケージ化の両方において非常に効率的です。wasm-bindgenとwasm-packを使うことで、RustからWASMへのコンパイルやTypeScriptの型定義生成が容易になります。
wasm-bindgenの使用
wasm-bindgenは、RustとJavaScript間のインターフェースを生成するためのツールです。以下のコマンドでインストールします。
cargo add wasm-bindgen
wasm-packの使用
wasm-packは、RustからWASMへのコンパイルとパッケージングを一括で行うツールです。公式サイトからインストールできます。
Rustでのadd関数の実装
以下のRustコードで、二つの数を加算する簡単なadd関数をWASMで実装します。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(left: usize, right: usize) -> usize {
left + right
}
npmを使用したwasmモジュールの構築
このwasmモジュールをnpmを使用して構築し、インストールするために、package.jsonに設定を追加します。
/* package.json の一部 */
{
"scripts": {
"build:wasm": "cd rust/wasm && wasm-pack build --target web --out-dir ../pkg"
},
"dependencies": {
"wasm": "file:rust/pkg"
}
}
WebWorkerの構築
WebWorkerは、Webアプリケーションのパフォーマンスを向上させるために使う場合があります。ここでWebWorkerでTypeScriptやWASMを利用できるかどうかは重要な関心事です。幸いWebpack5以降では、WASMおよびTypeScriptを直接WebWorkerに読み込むことができます。SWCでも検証しましたが同様の機能を実現することができました。
また、Comlinkを使用して、WebWorkerをシンプルな非同期関数の集合体として扱います。これにより、Workerとの通信が簡略化されます。
WebWorkerの実装例
以下のTypeScriptコードは、WebWorkerでのWASMの使用例を示しています。
import * as Comlink from 'comlink'
type SampleWorkerApi = {
add: (left: number, right: number) => Promise<number>
}
export class SampleWorker {
worker!: Worker
api!: Comlink.Remote<SampleWorkerApi>
constructor() {
this.initializeWorker()
}
initializeWorker() {
this.worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module',
})
this.api = Comlink.wrap(this.worker)
}
terminate() {
this.worker.terminate()
}
restart() {
this.terminate()
this.initializeWorker()
}
}
/* workerのTypeScriptコード例 */
import * as Comlink from 'comlink'
import init, { add } from 'wasm'
let isInitialized = false
init().then(() => {
isInitialized = true
})
const api = {
async add(left: number, right: number) {
if (!isInitialized) {
throw new Error('WASM module is not ready')
}
return add(left, right)
},
}
Comlink.expose(api)
アプリケーションの作成
上記の準備が整えば、Reactを使用してアプリケーションを作成できます。今回は簡単な足し算アプリケーションを示します。
/* Reactアプリケーションのコード例 */
import { useCallback, useEffect, useRef, useState } from 'react'
import { SampleWorker } from './worker'
import './App.css'
function App() {
const a = useRef<HTMLInputElement>(null)
const b = useRef<HTMLInputElement>(null)
const worker = useRef<SampleWorker>()
const [result, setResult] = useState(0)
const handleClick = useCallback(async () => {
if (a.current && b.current && worker.current) {
const left = parseInt(a.current.value)
const right = parseInt(b.current.value)
const res = await worker.current.api.add(left, right)
setResult(res)
}
}, [])
useEffect(() => {
// webworkerの作成と破棄
if (!worker.current) {
worker.current = new SampleWorker()
}
return () => {
if (worker.current) {
worker.current.terminate()
worker.current = undefined
}
}
})
return (
<>
<div className="card">
<input type="text" ref={a} />
+
<input type="text" ref={b} />
=
<input type="text" value={result} readOnly={true} />
<button onClick={handleClick}>calc</button>
</div>
</>
)
}
export default App
成果物とまとめ
このプロジェクトはGitHubで公開しています。適切なパッケージをインストールし、Viteを実行すれば、下図のような足し算アプリケーションが動作します。
プロジェクトリンク: GitHub - kackyt/react-wasm-worker-template
最新の技術を活用することで、フロントエンドの開発がよりシームレスになることを、この記事を通じて感じていただければ幸いです。
Discussion