WebAssembly での Ruby on Rails、フルスタックのブラウザ内開発

Vladimir Dementyev
Vladimir Dementyev

公開日: 2025 年 1 月 31 日

ブラウザで、フロントエンドだけでなくバックエンドも含めて、完全に機能するブログを実行するとします。サーバーやクラウドは不要です。必要なのは、ユーザー、ブラウザ、WebAssembly だけです。WebAssembly は、サーバーサイド フレームワークをローカルで実行できるようにすることで、従来のウェブ開発の境界を曖昧にし、新しい可能性を開いています。この投稿では、Vladimir Dementyev(Evil Martians のバックエンド ヘッド)が、Ruby on Rails を Wasm とブラウザに対応させるための進捗状況を共有します。

  • 15 分で Rails をブラウザに導入する方法。
  • Rails の wasm 化の裏側。
  • Rails と Wasm の将来。

Ruby on Rails の有名な「15 分でブログ」がブラウザで実行可能に

Ruby on Rails は、デベロッパーの生産性と迅速なリリースに重点を置いたウェブ フレームワークです。これは、GitHubShopify などの業界リーダーが使用しているテクノロジーです。このフレームワークの人気は、David Heinemeier Hansson(DHH)が公開した有名な「15 分でブログを作成する方法」動画のリリースから始まりました。2005 年当時、これほど短い時間で完全に機能するウェブ アプリケーションを構築することは考えられませんでした。まるで魔法のようでした。

今日は、ブラウザで完全に動作する Rails アプリケーションを作成して、この魔法のような感覚を取り戻したいと思います。まず、通常の方法で基本的な Rails アプリケーションを作成し、Wasm 用にパッケージ化します。

背景: コマンドラインで「15 分でブログ」

マシンに Ruby と Ruby on Rails がインストールされていることを前提として、まず新しい Ruby on Rails アプリケーションを作成し、いくつかの機能をスキャフォールディングします(元の「15 分でブログ」動画と同じように)。


$ rails new --css=tailwind web_dev_blog

  create  .ruby-version
  ...

$ cd web_dev_blog

$ bin/rails generate scaffold Post title:string date:date body:text

  create    db/migrate/20241217183624_create_posts.rb
  create    app/models/post.rb
  ...

$ bin/rails db:migrate

== 20241217183624 CreatePosts: migrating ====================
-- create_table(:posts)
   -> 0.0017s
== 20241217183624 CreatePosts: migrated (0.0018s) ===========

コードベースに変更を加えなくても、アプリケーションを実行して動作を確認できます。

$ bin/dev

=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000

これで、http://localhost:3000/postsブログを開き、投稿の作成を開始できます。

ブラウザで実行されているコマンドラインから起動された Ruby on Rails ブログ。

非常にシンプルですが、機能するブログ アプリケーションが数分で作成されました。これはフルスタックのサーバー制御アプリケーションです。データの保存に使用するデータベース(SQLite)、HTTP リクエストを処理するウェブサーバー(Puma)、ビジネス ロジックを保持し、UI を提供し、ユーザー操作を処理する Ruby プログラムがあります。最後に、ブラウジング エクスペリエンスを効率化するための JavaScript の薄いレイヤ(Turbo)があります。

公式の Rails デモでは、このアプリケーションをベアメタル サーバーにデプロイして本番環境対応にします。学習は反対の方向に進みます。アプリケーションを遠く離れた場所に配置するのではなく、ローカルに「デプロイ」します。

次のレベル: Wasm で「15 分でブログ」

WebAssembly が追加されて以来、ブラウザは JavaScript コードだけでなく、Wasm にコンパイル可能な任意のコードを実行できるようになりました。Ruby も例外ではありません。Rails は Ruby 以上のものです。違いを詳しく説明する前に、デモを続行して、Rails アプリケーションを wasmifywasmify-rails ライブラリで作られた動詞)しましょう。

ブログ アプリケーションを Wasm モジュールにコンパイルしてブラウザで実行するには、いくつかのコマンドを実行するだけです。

まず、Bundler(Ruby の npm)を使用して wasmify-rails ライブラリをインストールし、Rails CLI を使用してそのジェネレータを実行します。

$ bundle add wasmify-rails

$ bin/rails wasmify:install

  create  config/wasmify.yml
  create  config/environments/wasm.rb
  ...
  info   The application is prepared for Wasm-ificaiton!

wasmify:rails コマンドは、デフォルトの「開発」、「テスト」、「本番環境」の環境に加えて、専用の「wasm」実行環境を構成し、必要な依存関係をインストールします。グリーンフィールドの Rails アプリケーションの場合、これで Wasm に対応できます。

次に、Ruby ランタイム、標準ライブラリ、すべてのアプリケーション依存関係を含むコア Wasm モジュールをビルドします。

$ bin/rails wasmify:build

==> RubyWasm::BuildSource(3.3) -- Building
...
==> RubyWasm::CrossRubyProduct(ruby-3.3-wasm32-unknown-wasip1-full-4aaed4fbda7afe0bdf4e22167afd101e) -- done in 47.37s
INFO: Packaging gem: rake-13.2.1
...
INFO: Packaging gem: wasmify-rails-0.2.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 73.77 MB

このステップには時間がかかる場合があります。サードパーティ ライブラリからネイティブ拡張機能(C で記述)を適切にリンクするには、Ruby をソースからビルドする必要があります。この(一時的な)デメリットについては、後ほど説明します。

コンパイルされた Wasm モジュールは、アプリケーションの基盤にすぎません。また、アプリケーション コード自体とすべてのアセット(画像、CSS、JavaScript など)をパッケージ化する必要があります。パッケージ化を行う前に、ブラウザで wasm に変換された Rails を実行するために使用できる基本的なランチャー アプリケーションを作成します。そのため、生成ツールのコマンドもあります。

$ bin/rails wasmify:pwa

  create  pwa
  create  pwa/boot.html
  create  pwa/boot.js
  ...
  prepend  config/wasmify.yml

上記のコマンドは、Vite でビルドされた最小限の PWA アプリケーションを生成します。このアプリケーションは、ローカルで使用してコンパイルされた Rails Wasm モジュールをテストしたり、静的にデプロイしてアプリを配布したりできます。

ランチャーを使用すると、アプリケーション全体を単一の Wasm バイナリにパックするだけで済みます。

$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB

これで、ランチャー アプリを実行すると、ブラウザ内で完全に実行されている Rails ブログ アプリケーションが表示されます。

$ cd pwa/

$ yarn dev

  VITE v4.5.5  ready in 290 ms

    Local:   http://localhost:5173/

http://localhost:5173 に移動し、[Launch] ボタンが有効になるまで待ってからクリックします。ブラウザでローカルに実行されている Rails アプリを操作できます。

別のブラウザタブで実行されているブラウザタブから起動された Ruby on Rails ブログ。

マシンだけでなくブラウザのサンドボックス内でモノリシックなサーバーサイド アプリケーションを実行するのは、魔法のようではありませんか?私(「魔術師」であるにもかかわらず)には、まだファンタジーのように見えます。しかし、そこには魔法は存在せず、技術の進歩のみがあります。

デモ

この記事に埋め込まれたデモを試すことができます。また、スタンドアロン ウィンドウでデモを起動することもできます。GitHub のソースコードを確認する。

Wasm での Rails の内部

サーバーサイド アプリケーションを Wasm モジュールにパッケージ化する際の課題(と解決策)を理解するために、この記事の残りの部分では、このアーキテクチャのコンポーネントについて説明します。

ウェブ アプリケーションは、アプリケーション コードの記述に使用されるプログラミング言語だけでなく、多くのものに依存します。また、各コンポーネントをローカル デプロイ環境(ブラウザ)に配置する必要があります。「15 分でブログ」のデモの魅力は、アプリケーション コードを書き換えなくても実現できることです。従来のサーバーサイド モードとブラウザでアプリケーションを実行するために、同じコードが使用されました。

Ruby on Rails アプリを構成するコンポーネント: ウェブサーバー、データベース、キュー、ストレージ。また、Ruby のコア コンポーネント(gem、ネイティブ拡張機能、システム ツール、Ruby VM)も含まれています。

Ruby on Rails などのフレームワークには、インフラストラクチャ コンポーネントと通信するためのインターフェース(抽象化)が用意されています。次のセクションでは、フレームワーク アーキテクチャを使用して、やや難解なローカル サービング ニーズを満たす方法について説明します。

基盤: ruby.wasm

Ruby は 2022 年に Wasm に対応しました(バージョン 3.2.0 以降)。つまり、C ソースコードを Wasm にコンパイルし、任意の場所に Ruby VM を配置できるようになりました。ruby.wasm プロジェクトは、ブラウザ(または他の JavaScript ランタイム)で Ruby を実行するための事前コンパイル済みモジュールと JavaScript バインディングを提供します。ruby:wasm プロジェクトには、追加の依存関係を使用してカスタム Ruby バージョンをビルドできるビルドツールも付属しています。これは、C 拡張機能を持つライブラリに依存するプロジェクトにとって非常に重要です。はい。ネイティブ拡張機能を Wasm にコンパイルすることもできます。(まだすべての拡張機能ではありませんが、ほとんどの拡張機能が対象です)。

現在、Ruby は WebAssembly システム インターフェース WASI 0.1 を完全にサポートしています。コンポーネント モデルを含む WASI 0.2 はすでにアルファ版であり、完成まであと数歩です。WASI 0.2 がサポートされると、新しいネイティブ依存関係を追加するたびに言語全体を再コンパイルする必要がなくなります。コンポーネント化できるようになります。

副作用として、コンポーネント モデルはバンドルサイズの削減にも役立ちます。ruby.wasm の開発と進捗状況について詳しくは、What you can do with Ruby on WebAssembly をご覧ください。

これで、Wasm 方程式の Ruby 部分は解決しました。ただし、ウェブ フレームワークとしての Rails には、上の図に示すすべてのコンポーネントが必要です。他のコンポーネントをブラウザに配置し、Rails でリンクする方法については、以下をご覧ください。

ブラウザで実行されているデータベースに接続する

SQLite3 には公式の Wasm ディストリビューションと対応する JavaScript ラッパーが付属しているため、ブラウザに埋め込むことができます。Wasm 用 PostgreSQL は、PGlite プロジェクトで利用できます。したがって、Rails on Wasm アプリケーションからブラウザ内データベースに接続する方法のみを把握する必要があります。

データ モデリングとデータベースの操作を担当する Rails のコンポーネント(サブフレームワーク)は、Active Record と呼ばれます(ORM 設計パターンにちなんで命名されています)。Active Record は、データベース アダプタを使用して、実際の SQL 対応データベース実装をアプリケーション コードから抽象化します。Rails には、SQLite3、PostgreSQL、MySQL のアダプターが用意されています。ただし、これらはすべて、ネットワーク経由で利用可能な実際のデータベースへの接続を前提としています。この問題を解決するには、独自のアダプタを作成して、ブラウザ内のローカル データベースに接続します。

Wasmify Rails プロジェクトの一部として実装された SQLite3 Wasm アダプターと PGlite アダプターは、次のように作成されます。

  • アダプタ クラスは、対応する組み込みアダプタ(class PGliteAdapter < PostgreSQLAdapter など)から継承されるため、実際のクエリの準備と結果の解析ロジックを再利用できます。
  • 低レベルのデータベース接続の代わりに、JavaScript ランタイムにある外部インターフェース オブジェクト(Rails Wasm モジュールとデータベース間のブリッジ)を使用します。

たとえば、SQLite3 Wasm のブリッジ実装は次のとおりです。

export function registerSQLiteWasmInterface(worker, db, opts = {}) {
  const name = opts.name || "sqliteForRails";

  worker[name] = {
    exec: function (sql) {
      let cols = [];
      let rows = db.exec(sql, { columnNames: cols, returnValue: "resultRows" });

      return {
        cols,
        rows,
      };
    },

    changes: function () {
      return db.changes();
    },
  };
}

アプリケーションの観点から、実際のデータベースからブラウザ内のデータベースへの移行は、単なる構成の問題です。

# config/database.yml
development:
  adapter: sqlite3

production:
  adapter: sqlite3

wasm:
  adapter: sqlite3_wasm
  js_interface: "sqliteForRails"

ローカル データベースの操作は、それほど手間がかかりません。ただし、信頼できる一元的な情報源との間でデータの同期が必要な場合は、より高度な課題に直面する可能性があります。この質問はこの記事の範囲外です(ヒント: Rails on PGlite と ElectricSQL のデモをご覧ください)。

ウェブサーバーとしてのサービス ワーカー

ウェブ アプリケーションに不可欠なもう 1 つのコンポーネントは、ウェブサーバーです。ユーザーは HTTP リクエストを使用してウェブ アプリケーションを操作します。そのため、ナビゲーションやフォーム送信によってトリガーされた HTTP リクエストを Wasm モジュールに転送する方法が必要です。幸い、ブラウザにはその問題に対する解決策があります。それがサービス ワーカーです。

サービス ワーカーは、JavaScript アプリケーションとネットワーク間のプロキシとして機能する特別な種類の Web Worker です。リクエストをインターセプトして操作できます。たとえば、キャッシュに保存されたデータを提供する、他の URL にリダイレクトする、Wasm モジュールにリダイレクトするなどです。以下は、Wasm で実行されている Rails アプリケーションを使用してリクエストを処理するサービスの概要です。

// The vm variable holds a reference to the Wasm module with a
// Ruby VM initialized
let vm;
// The db variable holds a reference to the in-browser
// database interface
let db;

const initVM = async (progress, opts = {}) => {
  if (vm) return vm;
  if (!db) {
    await initDB(progress);
  }
  vm = await initRailsVM("/app.wasm");
  return vm;
};

const rackHandler = new RackHandler(initVM});

self.addEventListener("fetch", (event) => {
  // ...
  return event.respondWith(
    rackHandler.handle(event.request)
  );
});

「フェッチ」は、ブラウザからリクエストが送信されるたびにトリガーされます。リクエスト情報(URL、HTTP ヘッダー、本文)を取得して、独自のリクエスト オブジェクトを作成できます。

Rails は、ほとんどの Ruby ウェブ アプリケーションと同様に、HTTP リクエストの処理に Rack インターフェースに依存しています。Rack インターフェースは、リクエスト オブジェクトとレスポンス オブジェクトの形式と、基盤となる HTTP ハンドラ(アプリケーション)のインターフェースを記述します。これらのプロパティは次のように表すことができます。

request = {
   "REQUEST_METHOD" => "GET",
   "SCRIPT_NAME"    => "",
   "SERVER_NAME"  => "localhost",
   "SERVER_PORT" => "3000",
   "PATH_INFO"      => "/posts"
}

handler = proc do |env|
  [
    200,
    {"Content-Type" => "text/html"},
    ["<!doctype html><html><body>Hello Web!</body></html>"]
  ]
end

handler.call(request) #=> [200, {...}, [...]]

リクエスト形式に親しみを感じた場合は、以前に CGI を扱ったことがあるかもしれません。

RackHandler JavaScript オブジェクトは、JavaScript と Ruby の領域間でリクエストとレスポンスを変換します。Rack はほとんどの Ruby ウェブ アプリケーションで使用されているため、実装は Rails に固有ではなく汎用になります。ただし、実際の実装は長すぎるため、ここに投稿することはできません。

サービス ワーカーは、ブラウザ内ウェブ アプリケーションの重要な要素の一つです。HTTP プロキシだけでなく、キャッシュ レイヤとネットワーク スイッチャーでもあります(つまり、ローカルファーストまたはオフライン対応のアプリを構築できます)。これは、ユーザーがアップロードしたファイルを提供する際にも役立つコンポーネントです。

ブラウザにアップロードしたファイルを保持する

新しいブログ アプリケーションに最初に追加する機能の 1 つは、ファイルのアップロード、具体的には投稿への画像の添付のサポートです。そのためには、ファイルを保存して提供する方法が必要です。

Rails では、ファイルのアップロードを処理するフレームワークの部分をアクティブ ストレージと呼びます。Active Storage は、デベロッパーが低レベルのストレージ メカニズムを考えずにファイルを操作するための抽象化とインターフェースを提供します。ファイルをハードドライブに保存するかクラウドに保存するかにかかわらず、アプリケーション コードはファイルの存在を認識しません。

Active Record と同様に、カスタム ストレージ メカニズムをサポートするには、対応するストレージ サービス アダプタを実装するだけです。ブラウザでファイルを保存する場所

従来の方法では、データベースを使用します。はい。ファイルを blob としてデータベースに保存できます。追加のインフラストラクチャ コンポーネントは必要ありません。Rails には、そのための既製のプラグイン(Active Storage Database)がすでにあります。ただし、WebAssembly 内で実行される Rails アプリケーションを介してデータベースに保存されているファイルを提供する方法は、無料ではないシリアル化とシリアル化解除の処理が必要になるため、理想的ではありません。

より優れたブラウザ最適化ソリューションとしては、File System API を使用して、ファイル アップロードとサーバー アップロード ファイルをサービス ワーカーから直接処理します。このようなインフラストラクチャに最適な候補が OPFS(オリジン専用ファイル システム)です。これは最近登場したブラウザ API で、今後のブラウザ内アプリケーションで重要な役割を果たすでしょう。

Rails と Wasm を組み合わせて実現できること

この記事の冒頭で、サーバーサイド フレームワークをブラウザで実行する理由について疑問に思われたことでしょう。フレームワークやライブラリがサーバーサイド(またはクライアントサイド)であるという考えは、単なるラベルです。優れたコード、特に優れた抽象化はどこでも機能します。ラベルが、新しい可能性を探求したり、フレームワーク(Ruby on Rails など)やランタイム(WebAssembly)の限界を押し広げたりすることを妨げてはなりません。どちらも、このような型破りなユースケースでメリットを得ることができます。

従来型のユースケースや実用的なユースケースも数多くあります。

まず、フレームワークをブラウザに移植することで、学習とプロトタイピングの機会が大幅に広がります。ライブラリ、プラグイン、パターンをブラウザで他のユーザーと一緒に試すことができる世界を想像してみてください。Stackblitz では、JavaScript フレームワークでこれを実現できます。別の例として、ウェブページを離れることなく WordPress テーマを試すことができる WordPress Playground があります。Wasm は、Ruby とそのエコシステムで同様の機能を実現できます。

オープンソース デベロッパーにとって特に有用な、ブラウザ内コーディングの特殊なケースがあります。それは、問題の優先度付けとデバッグです。繰り返しになりますが、StackBlitz は JavaScript プロジェクト向けにこの機能を備えています。最小限の再現スクリプトを作成し、GitHub の問題のリンクを指定することで、メンテナンス担当者がシナリオを再現する時間を節約できます。実際、RunRuby.dev プロジェクトのおかげで、Ruby ではすでにこの取り組みが始まっています(ブラウザ内で再現して解決した問題の例をご覧ください)。

別のユースケースは、オフライン対応(またはオフライン対応)のアプリケーションです。通常はネットワークを使用して動作するが、接続がないときにも使用できるオフライン対応アプリ。たとえば、オフラインで受信トレイを検索できるメール クライアントなどです。または、[デバイスに保存] 機能を備えた音楽ライブラリ アプリを使用すると、ネットワーク接続がなくてもお気に入りの音楽を聴くことができます。どちらの例も、従来の PWA のようにキャッシュを使用するだけでなく、ローカルに保存されたデータに依存しています。

最後に、Rails でローカル(またはデスクトップ)アプリケーションを構築することも理にかなっています。フレームワークが提供する生産性はランタイムに依存しないためです。フル機能のフレームワークは、個人データやロジックが多いアプリケーションの構築に適しています。また、ポータブルな配信形式として Wasm を使用することも有効な選択肢です。

これは、Rails on Wasm の旅の始まりにすぎません。課題と解決策の詳細については、Ruby on Rails on WebAssembly 電子書籍をご覧ください(この書籍自体はオフライン対応の Rails アプリケーションです)。