お疲れ様です!IT業界で働くアライグマです!
先日、Zennで「Rust+Wasmで爆速ライフゲームを作って動く壁紙にする」という記事が話題になりました。1000×1000を超える巨大なグリッドでも60fpsで動作するという内容で、Rustの性能とWebAssemblyの可能性を示す好例です。
本記事では、この事例をベースに、RustでWebAssemblyアプリケーションを開発する際の最適化テクニックと、Windows環境でデスクトップ壁紙として動作させるまでの実装手順を解説します。WebAssembly(Wasm)の基礎から、JavaScript-Wasm間のデータ受け渡し最適化まで、実践的な内容をお届けします。
なぜRust+WebAssemblyなのか
WebAssemblyは、ブラウザ上でネイティブに近いパフォーマンスを実現できるバイナリフォーマットです。特にRustとの組み合わせは、以下の理由で相性が良いとされています。
Rust+Wasmの3つのメリット
- ゼロコスト抽象化:Rustのコンパイラ最適化がWasmバイナリにそのまま反映される
- メモリ安全性:ガベージコレクションなしで安全なメモリ管理が可能。Wasm環境でのメモリリークを防げる
- wasm-bindgenエコシステム:JavaScriptとの連携が容易。型安全なFFI(Foreign Function Interface)を自動生成できる
ライフゲームがWasm学習に適している理由
コンウェイのライフゲームは、シンプルなルールながら大量のセル状態を毎フレーム更新する必要があり、パフォーマンス最適化の題材として最適です。RustによるロジックとJavaScriptとの連携について詳しくは、webgl-crt-shaderでレトロCRTモニタ効果を再現するの記事でもWebGLとの連携パターンを解説しています。
IT女子 アラ美開発環境のセットアップ


ここでは、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との連携を考慮しましょう。



ライフゲームの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の記事で紹介したパフォーマンス最適化の考え方と共通する部分があります。



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コスト削減術の記事でも触れた考え方と同じです。



実装後の効果検証(ケーススタディ)
ここでは、実際に実装したライフゲームのパフォーマンス計測結果を紹介します。
状況(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)
- Rust移植:ライフゲームのロジック部分をRustで再実装。約100行のコード
- ゼロコピー最適化:cells_ptr()によるメモリ直接参照を導入
- ビット演算最適化:セル状態をu8からusize単位でまとめて処理するSIMD風の最適化を追加
- 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でも重要だった。



デスクトップ壁紙としての実行
最後に、作成したライフゲームをWindowsのデスクトップ壁紙として動作させる方法を紹介します。
Lively Wallpaperの活用
Windows向けの「Lively Wallpaper」というオープンソースツールを使えば、HTMLページをデスクトップ壁紙として表示できます。
- ライフゲームのHTMLファイルをビルド
- Lively WallpaperでそのHTMLを指定
- 全画面表示でデスクトップに設定
パフォーマンス設定の調整
壁紙として常時実行する場合は、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開発 |
| 難易度 | プロンプト作成中心 | コード記述あり |
| 補助金・給付金 | リスキリング補助金対象 | 教育訓練給付金対象 |
| おすすめ度 | 今の仕事に活かすなら | AIエンジニアになるなら |
| 公式サイト | 詳細を見る | − |



まとめ
Rust+WebAssemblyでライフゲームを実装し、パフォーマンス最適化のテクニックを解説しました。
- ゼロコピーが鍵:cells_ptr()でポインタを返し、JS側でメモリを直接参照する
- u8配列を使う:boolではなくu8を使うことで、メモリレイアウトが予測可能になる
- wasm-optで仕上げ:ビルド後にwasm-optを適用してバイナリサイズを削減する
- 壁紙化も可能:Lively Wallpaperを使えば、作ったものをデスクトップに表示できる
Rustの学習とWebブラウザでのパフォーマンス最適化を同時に体験できるプロジェクトとして、ライフゲームは最適です。まずは100×100から始めて、徐々にスケールアップしてみてください。













