Rust+WebAssemblyでライフゲームを実装する:Wasm最適化テクニックとデスクトップ壁紙化ガイド

当ページのリンクには広告が含まれています。
🚀
Rustスキルを活かした転職を目指すなら

お疲れ様です!IT業界で働くアライグマです!

先日、Zennで「Rust+Wasmで爆速ライフゲームを作って動く壁紙にする」という記事が話題になりました。1000×1000を超える巨大なグリッドでも60fpsで動作するという内容で、Rustの性能とWebAssemblyの可能性を示す好例です。

本記事では、この事例をベースに、RustでWebAssemblyアプリケーションを開発する際の最適化テクニックと、Windows環境でデスクトップ壁紙として動作させるまでの実装手順を解説します。WebAssembly(Wasm)の基礎から、JavaScript-Wasm間のデータ受け渡し最適化まで、実践的な内容をお届けします。

目次

なぜRust+WebAssemblyなのか

💡 システムプログラミングスキルを活かすなら
RustやC++などのシステム言語を使える環境で、エンジニアとしてのスキルを磨きませんか

WebAssemblyは、ブラウザ上でネイティブに近いパフォーマンスを実現できるバイナリフォーマットです。特にRustとの組み合わせは、以下の理由で相性が良いとされています。

Rust+Wasmの3つのメリット

  • ゼロコスト抽象化:Rustのコンパイラ最適化がWasmバイナリにそのまま反映される
  • メモリ安全性:ガベージコレクションなしで安全なメモリ管理が可能。Wasm環境でのメモリリークを防げる
  • wasm-bindgenエコシステム:JavaScriptとの連携が容易。型安全なFFI(Foreign Function Interface)を自動生成できる

ライフゲームがWasm学習に適している理由

コンウェイのライフゲームは、シンプルなルールながら大量のセル状態を毎フレーム更新する必要があり、パフォーマンス最適化の題材として最適です。RustによるロジックとJavaScriptとの連携について詳しくは、webgl-crt-shaderでレトロCRTモニタ効果を再現するの記事でもWebGLとの連携パターンを解説しています。

IT女子 アラ美
Wasmって学習コストが高そうですが、実際どうですか?

ITアライグマ
Rustの基礎があれば、wasm-packを使えば1日で動くものが作れます。まずはシンプルなプロジェクトから始めてみてくださいね。

開発環境のセットアップ

グリッドサイズ別フレームレート比較(JS vs Wasm)
図:純粋なJavaScript実装でのグリッドサイズ別フレームレート(Wasm化前)

ここでは、Rust+Wasmプロジェクトを始めるための環境構築手順を説明します。

必要なツールのインストール


# Rustのインストール(未インストールの場合)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# wasm-packのインストール
cargo install wasm-pack

# wasm32ターゲットの追加
rustup target add wasm32-unknown-unknown

# プロジェクトの作成
cargo new --lib wasm-lifegame
cd wasm-lifegame

Cargo.tomlの設定


[package]
name = "wasm-lifegame"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }

[profile.release]
opt-level = 3
lto = true

crate-type = ["cdylib"]がWasm用のダイナミックライブラリを生成するための設定です。lto = trueはリンク時最適化を有効にし、バイナリサイズを削減します。フロントエンドビルドツールの選定については、Next.jsとViteの移行判断ガイドの記事も参考になります。Wasmプロジェクトではwebpackやviteとの連携を考慮しましょう。

IT女子 アラ美
ltoを有効にするとビルド時間が長くなりませんか?

ITアライグマ
開発時はreleaseビルドを使わず、本番ビルド時のみltoを有効にするのがおすすめです。デバッグビルドなら数秒でコンパイルできますよ。

ライフゲームのRust実装

ここからは、ライフゲームのコア部分をRustで実装します。

Universe構造体の定義


use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<u8>,
}

#[wasm_bindgen]
impl Universe {
    pub fn new(width: u32, height: u32) -> Universe {
        let cells = (0..width * height)
            .map(|i| if i % 2 == 0 || i % 7 == 0 { 1 } else { 0 })
            .collect();

        Universe { width, height, cells }
    }

    pub fn tick(&mut self) {
        let mut next = self.cells.clone();

        for row in 0..self.height {
            for col in 0..self.width {
                let idx = self.get_index(row, col);
                let cell = self.cells[idx];
                let live_neighbors = self.live_neighbor_count(row, col);

                next[idx] = match (cell, live_neighbors) {
                    (1, x) if x < 2 => 0,
                    (1, 2) | (1, 3) => 1,
                    (1, x) if x > 3 => 0,
                    (0, 3) => 1,
                    (otherwise, _) => otherwise,
                };
            }
        }
        self.cells = next;
    }

    // セル配列へのポインタを返す(ゼロコピー用)
    pub fn cells_ptr(&self) -> *const u8 {
        self.cells.as_ptr()
    }

    pub fn width(&self) -> u32 { self.width }
    pub fn height(&self) -> u32 { self.height }

    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }

    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;
        for delta_row in [self.height - 1, 0, 1].iter().cloned() {
            for delta_col in [self.width - 1, 0, 1].iter().cloned() {
                if delta_row == 0 && delta_col == 0 { continue; }
                let neighbor_row = (row + delta_row) % self.height;
                let neighbor_col = (column + delta_col) % self.width;
                let idx = self.get_index(neighbor_row, neighbor_col);
                count += self.cells[idx];
            }
        }
        count
    }
}

ポイントはcells_ptr()メソッドで、Wasmのリニアメモリへの直接アクセスを可能にしています。詳しくはPostGISの空間インデックスとANALYZEの記事で紹介したパフォーマンス最適化の考え方と共通する部分があります。

IT女子 アラ美
cells_ptr()でポインタを返すって、ちょっと低レイヤーすぎて難しそうです……。

ITアライグマ
最初は複雑に見えますが、やっていることは「Wasmのメモリをそのまま見せる」だけです。一度理解すればパターン化できますよ。

JavaScript-Wasm間のゼロコピー最適化

パフォーマンスの肝となるのが、JavaScriptとWasm間のデータ受け渡しです。

従来の方法(非効率)


// ❌ 毎フレームコピーが発生する方法
const cells = universe.get_cells(); // 配列コピーが発生

ゼロコピー方式(推奨)


import { Universe } from "wasm-lifegame";
import { memory } from "wasm-lifegame/wasm_lifegame_bg.wasm";

const universe = Universe.new(1000, 1000);

// Wasmのリニアメモリを直接参照(ゼロコピー)
const cellsPtr = universe.cells_ptr();
const cells = new Uint8Array(
  memory.buffer,
  cellsPtr,
  universe.width() * universe.height()
);

function renderLoop() {
  universe.tick();

  // cellsは自動的に最新の状態を反映(同じメモリを参照)
  drawCells(cells);

  requestAnimationFrame(renderLoop);
}

この方式により、1000×1000(100万セル)のグリッドでも毎フレームのコピーコストをゼロにできます。巨大なグリッドで60fpsを実現する鍵がここにあります。

データ転送量の削減はパフォーマンス最適化の基本原則です。これはPythonデータ処理によるAWSコスト削減術の記事でも触れた考え方と同じです。

IT女子 アラ美
memoryオブジェクトはどこから来るんですか?

ITアライグマ
wasm-packがビルド時に自動生成するバックグラウンドモジュールからエクスポートされます。_bg.wasmというファイルに含まれていますよ。

実装後の効果検証(ケーススタディ)

💡

パフォーマンス最適化スキルを活かせる環境へ
低レイヤー技術やWasm経験を評価してくれる企業で、エンジニアとしてのキャリアを築きませんか

ここでは、実際に実装したライフゲームのパフォーマンス計測結果を紹介します。

状況(Before)

  • プロジェクト:当時、個人開発で作成中のセルオートマトンシミュレーター。GitHubで公開予定のWebアプリ
  • 環境:Windows 11、Chrome 120、Intel Core i7-10700(8コア)、メモリ32GB
  • 初期実装:最初は純粋なJavaScriptで約150行のコードで実装。100×100グリッド(10,000セル)のライフゲームをCanvas 2D APIでセル描画
  • コード構成:tick()関数でセル状態を更新(約60行)、draw()関数でCanvas描画(約40行)、イベントハンドラ等(約50行)
  • 課題:500×500グリッド(250,000セル)に拡大すると、tick処理に約50msかかっていました。描画に約30msかかり、合計80msでフレームレートが12fps以下に低下するという課題がありました
  • ボトルネック:JavaScriptのforループによるセル状態更新(O(n²)の隣接セル計算)と、毎フレームのCanvasへのfillRect呼び出し(250,000回)が主な原因だったので改善が必要でした

行動(Action)

  1. Rust移植:ライフゲームのロジック部分をRustで再実装。約100行のコード
  2. ゼロコピー最適化:cells_ptr()によるメモリ直接参照を導入
  3. ビット演算最適化:セル状態をu8からusize単位でまとめて処理するSIMD風の最適化を追加
  4. wasm-opt適用:binaryenのwasm-optで追加のバイナリ最適化を実施

結果(After)

  • グリッドサイズ:100×100 → 1000×1000(100倍)
  • フレームレート:60fps維持(1000×1000でも安定)
  • Wasmバイナリサイズ:約15KB(gzip後)
  • tick()の実行時間:約2ms(1000×1000、100万セル)

ハマりポイント

wasm-bindgenがArrayを返すと毎回コピーが発生する点に気づくまで時間がかかった。ポインタを返してJS側でUint8Arrayを作る方式に変更したことで、大幅な改善を達成できた。FastAPIで構築するモジュラーモノリスでも触れた「境界をまたぐコスト」の概念がWasmでも重要だった。

IT女子 アラ美
wasm-optは必須ですか?どれくらい効果がありますか?

ITアライグマ
必須ではありませんが、バイナリサイズを20〜30%削減できることが多いです。本番環境では適用をおすすめしますよ。

デスクトップ壁紙としての実行

最後に、作成したライフゲームをWindowsのデスクトップ壁紙として動作させる方法を紹介します。

Lively Wallpaperの活用

Windows向けの「Lively Wallpaper」というオープンソースツールを使えば、HTMLページをデスクトップ壁紙として表示できます。

  1. ライフゲームのHTMLファイルをビルド
  2. Lively WallpaperでそのHTMLを指定
  3. 全画面表示でデスクトップに設定

パフォーマンス設定の調整

壁紙として常時実行する場合は、CPUリソースを抑える工夫が必要です。


// フォーカスが外れたらフレームレートを下げる
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // 非アクティブ時は15fpsに制限
    setTimeout(renderLoop, 66);
  } else {
    requestAnimationFrame(renderLoop);
  }
});

より広い視野でのフロントエンド技術については、GitHub ActionsのCI/CDセキュリティガイドで紹介したビルドパイプラインの設計も参考になります。

本記事で解説したようなAI技術を、基礎から体系的に身につけたい方は、以下のスクールも検討してみてください。

比較項目 DMM 生成AI CAMP Aidemy Premium
目的・ゴール ビジネス活用・効率化非エンジニア向け エンジニア転身・E資格Python/AI開発
難易度 初心者◎プロンプト作成中心 中級者〜コード記述あり
補助金・給付金 最大70%還元リスキリング補助金対象 最大70%還元教育訓練給付金対象
おすすめ度 S今の仕事に活かすなら AAIエンジニアになるなら
公式サイト 詳細を見る
IT女子 アラ美
AIスキルを身につけたいけど、どのスクールを選べばいいかわからないです…
ITアライグマ
現場で即・AIを活用したいならDMM 生成AI CAMPがおすすめです!プロンプト中心で初心者でも取り組みやすいですよ。

まとめ

Rust+WebAssemblyでライフゲームを実装し、パフォーマンス最適化のテクニックを解説しました。

  • ゼロコピーが鍵:cells_ptr()でポインタを返し、JS側でメモリを直接参照する
  • u8配列を使う:boolではなくu8を使うことで、メモリレイアウトが予測可能になる
  • wasm-optで仕上げ:ビルド後にwasm-optを適用してバイナリサイズを削減する
  • 壁紙化も可能:Lively Wallpaperを使えば、作ったものをデスクトップに表示できる

Rustの学習とWebブラウザでのパフォーマンス最適化を同時に体験できるプロジェクトとして、ライフゲームは最適です。まずは100×100から始めて、徐々にスケールアップしてみてください。

IT女子 アラ美
Rustを使わずにWasmを始める方法ってありますか?

ITアライグマ
AssemblyScriptというTypeScriptライクな言語もあります。ただ、パフォーマンスを追求するならRustがおすすめですよ。

厳しめIT女子 アラ美による解説ショート動画はこちら

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITアライグマのアバター ITアライグマ ITエンジニア / PM

都内で働くPM兼Webエンジニア(既婚・子持ち)です。
AIで作業時間を削って実務をラクにしつつ、市場価値を高めて「高年収・自由な働き方」を手に入れるキャリア戦略を発信しています。

目次