WebAssembly (Wasm)はWebアプリケーションにおいて高速に演算処理を実行することを目的に開発されました。 主要なブラウザーがWasmに対応してすでに5年以上たち、グラフィック関連やエンタメの分野ではWasmの採用事例をたびたび目にします。Figmaがかなり早い段階でWebAssemblyの利用を開始した話は有名だと思います。ほかにはAmazon Prime Videoでの事例も面白いです。
ブラウザーなどの仮想マシン上で実行されるWasmバイナリは、C・Rustなどのプログラミン言語で書かれたコードをコンパイルすることで作成します。 Wasmバイナリは様々な言語からコンパイルすることができますが、ツールチェインが充実しているため新規の開発ではRustを選択するのが便利です。
今回はRustでFibonacci数を計算する関数を書いてWasmバイナリを生成し、Next.jsで作成したフロントエンドで実行してみました。
使用したコードはこのレポジトリにアップロードしています。 作成したWebアプリケーションのサンプルはGithub Pagesにデプロイしてあります。
開発環境にはNext.jsをビルドするために
RustのコードからWasmバイナリとJSへのbindingを生成するために
が必要です。
Toolchainはローカルにインストールしてもいいですし、ローカル環境を汚したくない場合はVSCodeでdevcontainerを使用すると簡単に開発環境を用意できます。
$ npx create-next-app --ts nextjs-wasm
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Next.jsプロジェクトが生成されるのでディレクトリを移動して、開発用サーバーを立ち上げると
http://localhost:3000
から初期ページが確認できます。
$ cd next-wasm
$ npm run dev
> [email protected] dev
> next dev
▲ Next.js 14.0.4
- Local: http://localhost:3000
今回はNext.jsのプロジェクト内にWasmのコードを配置するmonorepo構成で行きます。
$ wasm-pack new wasm
この段階でプロジェクトは以下のような構成になっているはずです。
.
├── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── next.config.js
├── next-env.d.ts
├── package.json
├── package-lock.json
├── postcss.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── README.md
├── tailwind.config.ts
├── tsconfig.json
└── wasm
├── Cargo.lock
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── src
├── target
└── tests
Wasmの関数をNext.jsで使用するにはwebpack
の設定が必要です。
設定内容はNext.jsのGithub Repositryにexampleがあります。
next.config.js
に
/** @type {import('next').NextConfig} */
const path = require("path");
const nextConfig = {
webpack(config, { isServer, dev }) {
// Use the client static directory in the server bundle and prod mode
// Fixes `Error occurred prerendering page "/"`
config.output.webassemblyModuleFilename =
isServer && !dev
? "../static/wasm/[modulehash].wasm"
: "static/wasm/[modulehash].wasm";
// Since Webpack 5 doesn't enable WebAssembly by default, we should do it manually
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};
module.exports = nextConfig;
を追加すればよいです。
wasmの関数はwasm
ディレクトリで
$ wasm-pack build
を実行することで生成できます。しかし、毎回手動で実行するのは手間ですし生成し忘れも発生するので
wasm-pack-pluginを利用してwebpack
のビルド時に自動的にWasmのビルドも行えるようにします。
まずプロジェクトにwasm-pack-plugin
を追加し、
$ npm install --save-dev @wasm-tool/wasm-pack-plugin
next.config.js
に設定を追加します。
設定追加後の設定ファイルは以下のようになります。
/** @type {import('next').NextConfig} */
const path = require("path");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const nextConfig = {
webpack(config, { isServer, dev }) {
config.plugins.push(
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, "./wasm"),
outDir: path.resolve(__dirname, "./wasm/pkg"),
})
);
// Use the client static directory in the server bundle and prod mode
// Fixes `Error occurred prerendering page "/"`
config.output.webassemblyModuleFilename =
isServer && !dev
? "../static/wasm/[modulehash].wasm"
: "static/wasm/[modulehash].wasm";
// Since Webpack 5 doesn't enable WebAssembly by default, we should do it manually
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};
module.exports = nextConfig;
この設定をいれてからNext.jsのプロジェクトをビルドすると自動的にwasm-pack build
も実行され、
wasm/pkg
にWasmの関数が生成されます。
フィボナッチ数を計算する関数をRustで書きます。
wasm/src/lib.rs
に
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fib(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fib(n - 2) + fib(n - 1),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fib0() {
assert_eq!(0, fib(0))
}
#[test]
fn test_fib1() {
assert_eq!(1, fib(1))
}
#[test]
fn test_fib3() {
assert_eq!(2, fib(3))
}
#[test]
fn test_fib5() {
assert_eq!(5, fib(5))
}
#[test]
fn test_fib20() {
assert_eq!(6765, fib(20))
}
}
Fibonacci数を数値的に計算する方法はもっと効率がよい手法がありますが、 今回はRustで書いたWasmの関数をNext.jsから使用することが目的なのでシンプルな方法にとどめておきます。
Wasm関数の用意ができたのでNext.jsのページを作成していきます。
まず、デフォルトのcssは邪魔になるので import './globals.css
をapp/layout.tsx
からコメントアウトしておきます。
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
//import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
次にページを用意します。
Wasmの関数はwasm-pack
がビルドの際に自動的にJS用のbinding codeを生成してくれるため
生成物のディレクトリから関数をインポートしてJSで使用します。
Wasmの関数は非同期に実行します。
今回はuseEffect
内で実行します。
app/page.tsx
を以下のように書き換えます。
"use client";
import { useState, useEffect } from "react";
export default function Home() {
const [inputNumber, setInputNumber] = useState(0);
const [fibnacci, setFibnacci] = useState(0);
useEffect(() => {
async function _fib() {
const { fib } = await import("../wasm/pkg");
setFibnacci(fib(inputNumber));
}
_fib();
}, [inputNumber]);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div>
<h1>Fibonacci</h1>
<h2>Input Number: {inputNumber}</h2>
<button
onClick={() => {
inputNumber > 0 ? setInputNumber(inputNumber - 1) : 0;
}}
>
-
</button>
<button onClick={() => setInputNumber(inputNumber + 1)}>+</button>
<h2>Fibonacci Nmber: {fibnacci}</h2>
</div>
</main>
);
}
これで最低限のコードの準備が完了しました。 開発サーバーを立ち上げてみましょう。
$ npm run dev
http://localhost:3000/
にアクセスすると
以下の様な画面が表示されます。
-
, +
ボタンでInput Numberを変更すると対応するFibonacci数が表示されます。
今回のFibonacci数計算のアルゴリズムは効率が良くないので40を過ぎたあたりから計算が苦しくなってきます。
これは余談ですが、今回作成したWebページではSSRを使用していないのでGithub Pagesにデプロイが可能なので Github Actionsで成果物をデプロイするようにしました。 デプロイ方法としてはこの記事を参考にしました。
今回作成したWebページではWasmをビルドする必要があるので記事に従って設定すると作成されるGithub Actionsの設定ファイル .github/workflows/nextjs.yml
にRustのtoolchainのインストールとwasm-pack
のインストールの設定を追加する必要があります。
Setup Node
の後に以下のような設定を追加すると必要なToolchainのインストールができます。
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: ${{ steps.detect-package-manager.outputs.manager }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup wasm-pack
id: setup-wasm-pack
run: |
cargo install wasm-pack
Rustで書いたFibonacci数を計算するコードをwasm-pack
でビルドしてNext.jsで書いたフロントエンドで使用してみました。
wasm-pack
のおかげでpure Rustで書いたコードをWasmバイナリとしてビルドすることは簡単です。
ネイティブコード並みの速度での演算をブラウザーで行えるようになることでサーバーサイドなしでできることの範囲が大きく広がります。
Toolchainが充実してきてWasmを使うことのハードルは下がってきているので、みなさんもJavascriptだとちょっと重いんだよなという処理をWasmを使用してフロントエンドで実行することを検討してみてはいかがでしょうか。