🌟

Vite+React+TypeScript+SWCでWebAssembly開発環境を試す

2023/12/23に公開

はじめに

こんにちは、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の使用例を示しています。

index.ts
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.ts
/* 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を使用してアプリケーションを作成できます。今回は簡単な足し算アプリケーションを示します。

App.tsx
/* 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