← Blog

Ruby in WebAssembly: ruby.wasm, PicoRuby.wasm, Spinel und Roundhouse

Ich arbeite viel mit ruby.wasm, zum Beispiel für die Ruby Koans im Browser. Dieses Jahr sind drei weitere Projekte dazugekommen, die Ruby in WebAssembly deutlich interessanter machen, und jedes geht einen anderen Weg:

Um die ersten drei zu vergleichen, habe ich in allen dasselbe kleine Programm laufen lassen: eine Schweizer Rechnung mit Mengenrabatt, 8,1% Mehrwertsteuer und Rundung auf 5 Rappen, dazu eine Wortzählung.

Item = Struct.new(:name, :quantity, :unit_price)

class Invoice
  VAT = 0.081

  def initialize(items)
    @items = items
  end

  def line_total(item)
    gross = item.quantity * item.unit_price
    item.quantity >= 10 ? gross * 0.9 : gross
  end

  def subtotal = @items.sum { |item| line_total(item) }
  def vat = subtotal * VAT
  def total = ((subtotal + vat) * 20).round / 20.0
end

invoice = Invoice.new([
  Item.new("Beratung", 2, 50.0),
  Item.new("Kaffee", 12, 10.0)
])

[["Subtotal", invoice.subtotal], ["MWST 8.1%", invoice.vat], ["Total", invoice.total]].each do |label, amount|
  puts format("%-10s %8.2f", label, amount)
end

counts = Hash.new(0)
"the quick brown fox jumps over the lazy dog the end".split.each { |w| counts[w] += 1 }
p counts.max_by { |_, n| n }

CRuby gibt aus:

Subtotal     208.00
MWST 8.1%     16.85
Total        224.85
["the", 3]

ruby.wasm: das echte CRuby

ruby.wasm gehört zum offiziellen Ruby-Projekt. Es ist CRuby selbst, kompiliert für WASI, und führt deshalb fast jeden Ruby-Code aus, samt Standardbibliothek und vielen Gems. Für den Anfang genügen zwei Script-Tags:

<script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
<script type="text/ruby">
  require "js"
  JS.global[:document].title = "Hello from #{RUBY_VERSION}"
</script>

Das Rechnungsprogramm läuft unverändert und liefert genau die Ausgabe von CRuby, denn es ist CRuby 4.0.

Der Preis sind Grösse und Startzeit. ruby+stdlib.wasm ist 31 MB gross, über das CDN komprimiert etwa 8,6 MB, und auf meinem Rechner erschien die Ausgabe 1,2 bis 2,5 Sekunden nach dem Laden der Seite. Dazu kommen ein paar Einschränkungen: WebAssembly kennt keine Threads, Bibliotheken, die einen Thread starten, brauchen also einen kleinen Umweg (einen solchen beschreibe ich für Kumi), und Gems mit C-Extensions müssen mit dem Werkzeug rbwasm in die .wasm-Datei hineinkompiliert werden, statt dass man sie installiert.

Geeignet für: Tutorials und Playgrounds, bestehende Ruby-Bibliotheken im Browser, überall dort, wo Kompatibilität wichtiger ist als die Ladezeit.

PicoRuby.wasm: klein und schnell

PicoRuby von Hitoshi Hasumi ist die kleinste Ruby-Implementierung, gemacht für Ein-Chip-Mikrocontroller wie den Raspberry Pi Pico. Die WebAssembly-Version verwendet die mruby-VM und ist als @picoruby/wasm-wasi veröffentlicht:

<script src="https://cdn.jsdelivr.net/npm/@picoruby/wasm-wasi@latest/dist/init.iife.js"></script>
<script type="text/ruby">
  require "js"
  button = JS.document.getElementById("btn")
  button.addEventListener("click") do |event|
    puts "clicked at #{event[:clientX]}, #{event[:clientY]}"
  end
</script>

picoruby.wasm ist 2,1 MB gross, komprimiert etwa 0,9 MB, und mein Programm zeigte seine Ausgabe etwa 80 Millisekunden nach dem Laden der Seite. Die Brücke zu JavaScript ist gut durchdacht: setTimeout, fetch und Event-Listener nehmen Ruby-Blöcke entgegen und laufen als kooperative Tasks, ohne die Grösse, die Asyncify von Emscripten kosten würde. Es gibt sogar eine Erweiterung für die Chrome DevTools mit binding.irb-Breakpoints und ein Komponenten-Framework, Funicular.

PicoRuby ist allerdings eine Teilmenge von Ruby. Mein Programm brauchte drei Änderungen, bevor es lief: Es gibt kein Struct, kein Array#sum und kein Hash#max_by. Mit einer kleinen Klasse, inject und einer Schleife stattdessen waren die Resultate dieselben. Endless Methods und format funktionierten auf Anhieb.

Geeignet für: interaktive Seiten, auf denen Ruby JavaScript ersetzt und ein schneller Start wichtiger ist als die ganze Standardbibliothek.

Spinel: Ruby im Voraus kompiliert

Spinel ist der Ruby-Compiler von Matz. Er parst Ruby mit prism, leitet die Typen des ganzen Programms her, erzeugt C-Code und kompiliert ihn mit dem C-Compiler des Systems. Das Resultat ist eine eigenständige Datei ohne Interpreter. Die erste Version erschien am 12. September 2026, und das Projekt meldet auf seinen Benchmarks im geometrischen Mittel etwa die 8,5-fache Geschwindigkeit von CRuby mit YJIT.

Spinel kann auch WebAssembly erzeugen. Mit installiertem wasi-sdk ist das eine einzige Option:

$ spinel invoice.rb -o invoice                        # nativ, 421 KB
$ spinel --target=wasm32-wasi invoice.rb -o invoice.wasm   # 963 KB, 322 KB mit gzip

Das Rechnungsprogramm liess sich ohne eine einzige Änderung kompilieren, und sowohl die native Datei als auch das .wasm-Modul gaben genau das aus, was CRuby ausgab. Das Modul habe ich mit Node.js 25 und dessen eingebauter WASI-Unterstützung ausgeführt. Der ganze Lauf dauerte 0,18 Sekunden, das meiste davon der Start von Node. Die native Datei war nach weniger als 10 Millisekunden fertig, CRuby brauchte 0,2 Sekunden.

Was das möglich macht, ist zugleich die Grenze: Spinel muss das ganze Programm beim Kompilieren kennen. Es gibt kein eval, kein method_missing, und send und define_method funktionieren nur mit Namen, die im Quelltext stehen. Beim WebAssembly-Ziel sind Ganzzahlen 32 Bit gross, es gibt keine Fibers und Threads, und raise/rescue braucht eine Laufzeitumgebung mit WebAssembly-Exception-Handling, das aktuelle Browser, Node.js und wasmtime haben.

Geeignet für: Kommandozeilen-Werkzeuge, rechenintensiven Code, kleine eigenständige Programme und Ruby-Logik, die als .wasm-Modul laufen soll, ohne einen Interpreter mitzuliefern.

Roundhouse: Rails als Spezifikation

Roundhouse von Sam Ruby setzt eine Ebene höher an. Es liest eine unveränderte Rails-Applikation und ermittelt ihre Typen durch Analyse des ganzen Programms, ohne Typannotationen, ohne laufende App und ohne Datenbank. has_many :comments sagt bereits, welche Klasse eine Assoziation zurückgibt, und genau das nutzt Roundhouse.

Die Analyse wird auf drei Arten verwendet:

Rails ist die erste Eingabe, aber nicht die einzige. Zusammen mit Jeremy Evans, der Roda und Sequel betreut, hat Sam Ruby Roda + Sequel als zweites Quell-Framework hinzugefügt: Ein kleiner Roda-Blog läuft bereits durch dieselbe Pipeline und besteht danach seine eigenen Tests. Ein Frontend für Hanami wird diskutiert. Und die Framework-Laufzeit, die die erzeugten Apps verwenden, ihr eigenes Active Record, Action Controller und Action View, ist selbst in Ruby geschrieben und wird von derselben Pipeline in jede Zielsprache übersetzt. Ihre Testsuite läuft in Crystal, Kotlin, Swift, TypeScript, Ruby und Spinel durch.

Der WebAssembly-Teil ist die IDE im Browser. Roundhouse ist in Rust geschrieben, und die Analyse ist zu WebAssembly kompiliert und läuft in einem Web Worker. Sie können eine Rails-App von Ihrer eigenen Festplatte öffnen, und nichts wird hochgeladen. Mastodon mit etwa 1100 Dateien ist in wenigen Sekunden analysiert, danach gibt es Typen beim Darüberfahren mit der Maus, den Sprung von einer View zur Controller-Action, die ihre Instanzvariablen setzt, und Request-Traces. Ausserdem gibt es ein Ziel, das die TypeScript-Version einer App für den Browser bündelt, mit SQLite als WebAssembly.

Geeignet für: eine Rails-Codebasis verstehen und prüfen, und ausloten, was aus einer Rails-App ausserhalb von CRuby werden könnte.

Im Vergleich

  ruby.wasm PicoRuby.wasm Spinel Roundhouse
Ansatz CRuby-Interpreter kleine VM auf mruby-Basis Ahead-of-Time-Compiler Analyse und Transpiler
Abgedecktes Ruby fast alles eine Teilmenge alles ausser dynamischen Features Rails-Apps, Roda + Sequel im Aufbau
Mein Programm lief unverändert drei Änderungen lief unverändert —
Download ~8,6 MB komprimiert ~0,9 MB komprimiert 322 KB mit gzip (ganzes Programm) —
Läuft in Browser, Node.js, WASI Browser nativ, WASI Kommandozeile, Browser-IDE

Diese vier sind eigentlich keine Konkurrenten. ruby.wasm bringt die volle Kompatibilität von Ruby in den Browser. PicoRuby.wasm macht Ruby leicht genug, um auf einer Seite JavaScript zu ersetzen. Spinel zeigt, dass sich Ruby-Programme wie C-Programme kompilieren lassen, bis hinunter zu einem WebAssembly-Modul von ein paar hundert Kilobyte. Und Roundhouse zeigt, wie viel eine statische Analyse über eine Web-App wissen kann, angefangen bei Rails. Für Ruby-Entwickler ist 2026 ein sehr gutes Jahr für WebAssembly.

Die Zeiten oben stammen von meinem Entwicklungsrechner, der gleichzeitig mit anderem beschäftigt war. Verstehen Sie sie deshalb als grobe Eindrücke und nicht als Benchmark. Für einen richtigen Vergleich der Startzeiten misst sue445s ruby-wasm-startup-benchmark ruby.wasm und PicoRuby.wasm im Browser.