All Articles

Next.jsでWasmの関数を利用

はじめに

WebAssembly (Wasm)はWebアプリケーションにおいて高速に演算処理を実行することを目的に開発されました。 主要なブラウザーがWasmに対応してすでに5年以上たち、グラフィック関連やエンタメの分野ではWasmの採用事例をたびたび目にします。Figmaがかなり早い段階でWebAssemblyの利用を開始した話は有名だと思います。ほかにはAmazon Prime Videoでの事例も面白いです。

ブラウザーなどの仮想マシン上で実行されるWasmバイナリは、C・Rustなどのプログラミン言語で書かれたコードをコンパイルすることで作成します。 Wasmバイナリは様々な言語からコンパイルすることができますが、ツールチェインが充実しているため新規の開発ではRustを選択するのが便利です。

今回はRustでFibonacci数を計算する関数を書いてWasmバイナリを生成し、Next.jsで作成したフロントエンドで実行してみました。

使用したコードはこのレポジトリにアップロードしています。 作成したWebアプリケーションのサンプルはGithub Pagesにデプロイしてあります。

目次

Toolchain

開発環境にはNext.jsをビルドするために

  • Nodejs

RustのコードからWasmバイナリとJSへのbindingを生成するために

が必要です。

Toolchainはローカルにインストールしてもいいですし、ローカル環境を汚したくない場合はVSCodeでdevcontainerを使用すると簡単に開発環境を用意できます。

Projectの作成

$ 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

Webpackの設定

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の関数が生成されます。

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から使用することが目的なのでシンプルな方法にとどめておきます。

Pageの用意

Wasm関数の用意ができたのでNext.jsのページを作成していきます。

まず、デフォルトのcssは邪魔になるので import './globals.cssapp/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/にアクセスすると 以下の様な画面が表示されます。

nextjs

-, +ボタンでInput Numberを変更すると対応するFibonacci数が表示されます。 今回のFibonacci数計算のアルゴリズムは効率が良くないので40を過ぎたあたりから計算が苦しくなってきます。

Github PagesへのDeploy

これは余談ですが、今回作成した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を使用してフロントエンドで実行することを検討してみてはいかがでしょうか。

Published Dec 31, 2023

スタートアップで働くデータエンジニア兼データサイエンティスト。興味の範囲はデータパイプラインの構築、データ分析、機械学習、クラウドなどなど。