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:
- ruby.wasm kompiliert den echten CRuby-Interpreter zu WebAssembly.
- PicoRuby.wasm kompiliert ein kleines Ruby, das für Mikrocontroller gemacht ist.
- Spinel, der neue Ahead-of-Time-Compiler von Matz, macht aus einem Ruby-Programm C-Code und daraus eine native Datei oder ein
.wasm-Modul. - Roundhouse von Sam Ruby liest ganze Web-Applikationen, allen voran Rails, und führt seine Analyse als WebAssembly im Browser aus.
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:
- Prüfen: Typfehler, nil-Sicherheit und N+1-Abfragen, auf der Kommandozeile, als Editor-Plugin (LSP) oder für einen KI-Agenten (MCP).
- Transpilieren: die App als eigenständiges Projekt in Rust, Go, TypeScript, Crystal, Elixir, Kotlin, Swift, Python, C# oder einfachem Ruby. Jedes Ziel wird Seite für Seite gegen die echte Rails-App getestet.
- Kompilieren: Mit Spinel wird die App zu einer einzigen nativen Datei. Die Chat-App Campfire von Basecamp läuft so und besteht ihre eigene Testsuite.
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.
Links
- ruby.wasm und seine Dokumentation (Englisch)
- PicoRuby und PicoRuby.wasm auf npm
- Spinel und seine Hinweise zu WebAssembly (Englisch)
- Roundhouse, die Browser-IDE und Sam Rubys Artikel Campfire Chats (Englisch)