Ruby in WebAssembly: ruby.wasm, PicoRuby.wasm, Spinel and Roundhouse
I use ruby.wasm a lot, for example for Ruby Koans in the browser. This year, three more projects have made Ruby in WebAssembly a lot more interesting, and each one takes a different path:
- ruby.wasm compiles the real CRuby interpreter to WebAssembly.
- PicoRuby.wasm compiles a small Ruby made for microcontrollers.
- Spinel, Matz’s new ahead-of-time compiler, turns a Ruby program into C, and from there into a native binary or a
.wasmmodule. - Roundhouse, by Sam Ruby, reads whole web applications, Rails first, and runs its analysis in the browser as WebAssembly.
To compare the first three, I ran the same small program in each of them: a Swiss invoice with a bulk discount, 8.1% VAT and rounding to 5 Rappen, plus a word count.
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 prints:
Subtotal 208.00
MWST 8.1% 16.85
Total 224.85
["the", 3]
ruby.wasm: the real CRuby
ruby.wasm is part of the official Ruby project. It is CRuby itself, compiled for WASI, so it runs almost any pure Ruby code, including the standard library and many gems. Two script tags are enough to get started:
<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>
The invoice program runs unchanged and gives exactly the CRuby output, because it is CRuby 4.0.
The price is size and startup time. ruby+stdlib.wasm is 31 MB, about 8.6 MB compressed over the CDN, and on my machine the output appeared 1.2 to 2.5 seconds after the page loaded. There are also a few limits: WebAssembly has no threads, so libraries that start a thread need a small workaround (I describe one for Kumi), and C extensions have to be compiled into the .wasm file with the rbwasm tool instead of being installed.
Good for: tutorials and playgrounds, running existing Ruby libraries in the browser, anything where compatibility matters more than load time.
PicoRuby.wasm: small and quick
PicoRuby by Hitoshi Hasumi is the smallest Ruby implementation, made for one-chip microcontrollers such as the Raspberry Pi Pico. Its WebAssembly build uses the mruby VM and is published as @picoruby/wasm-wasi:
<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 is 2.1 MB, or about 0.9 MB compressed, and my program printed its output about 80 milliseconds after the page loaded. The JavaScript bridge is well designed: setTimeout, fetch and event listeners take Ruby blocks and run as cooperative tasks, without the size cost of Emscripten’s Asyncify. There is even a Chrome DevTools extension with binding.irb breakpoints, and a component framework, Funicular.
PicoRuby is a subset of Ruby, though. My program needed three changes before it ran: there is no Struct, no Array#sum and no Hash#max_by. With a small class, inject and a loop instead, the results were the same. Endless methods and format worked out of the box.
Good for: interactive pages where Ruby replaces JavaScript, and where a fast start matters more than the full standard library.
Spinel: Ruby compiled ahead of time
Spinel is Matz’s Ruby compiler. It parses Ruby with prism, infers the types of the whole program, generates C and compiles it with the system C compiler. The result is a standalone binary without an interpreter. The first release came out on 12 September 2026, and the project reports a geometric mean of about 8.5 times the speed of CRuby with YJIT on its benchmarks.
Spinel also has a WebAssembly target. With the wasi-sdk installed, it is one flag:
$ spinel invoice.rb -o invoice # native, 421 KB
$ spinel --target=wasm32-wasi invoice.rb -o invoice.wasm # 963 KB, 322 KB gzipped
The invoice program compiled without a single change, and both the native binary and the .wasm module printed exactly what CRuby printed. I ran the module with Node.js 25 and its built-in WASI support. The whole run took 0.18 seconds, most of it Node starting up. The native binary finished in under 10 milliseconds, where CRuby needed 0.2 seconds.
What makes this possible is also the limit: Spinel needs to know the whole program at compile time. There is no eval, no method_missing, and send and define_method only work with names written in the source. On the WebAssembly target, integers are 32-bit, there are no fibers or threads, and raise/rescue needs a runtime with WebAssembly exception handling, which current browsers, Node.js and wasmtime have.
Good for: command-line tools, compute-heavy code, small self-contained programs, and Ruby logic that should run as a .wasm module without shipping an interpreter.
Roundhouse: Rails as a specification
Roundhouse by Sam Ruby works one level higher. It reads an unmodified Rails application and recovers its types by whole-program inference, without type annotations, a running app or a database. has_many :comments already says which class an association returns, and Roundhouse uses that.
The analysis is used in three ways:
- Check: type errors, nil safety and N+1 queries, from the command line, as an editor plugin (LSP) or for an AI agent (MCP).
- Transpile: the app as a standalone project in Rust, Go, TypeScript, Crystal, Elixir, Kotlin, Swift, Python, C# or plain Ruby. Each target is tested against the real Rails app, page by page.
- Compile: with Spinel, the app becomes one native binary. Basecamp’s chat app Campfire runs this way and passes its own test suite.
Rails is the first input, but not the only one. Together with Jeremy Evans, who maintains Roda and Sequel, Sam Ruby has added Roda + Sequel as a second source framework: a small Roda blog already goes through the same pipeline and passes its own tests afterwards. A Hanami front end is being discussed. And the framework runtime that the generated apps use, their own Active Record, Action Controller and Action View, is itself written in Ruby and translated into each target language by the same pipeline. Its test suite passes in Crystal, Kotlin, Swift, TypeScript, Ruby and Spinel.
The WebAssembly part is the in-browser IDE. Roundhouse is written in Rust, and the analyzer is compiled to WebAssembly and runs in a Web Worker. You can open a Rails app from your own disk, and nothing is uploaded. Mastodon, with about 1,100 files, is analyzed in a few seconds, and then you get types on hover, go-to-definition from a view to the controller action that sets its instance variables, and request traces. There is also a target that bundles the TypeScript version of an app to run in the browser, with SQLite compiled to WebAssembly.
Good for: understanding and checking a Rails codebase, and exploring what a Rails app could become outside CRuby.
Side by side
| ruby.wasm | PicoRuby.wasm | Spinel | Roundhouse | |
|---|---|---|---|---|
| Approach | CRuby interpreter | small mruby-based VM | ahead-of-time compiler | analyzer and transpiler |
| Ruby covered | almost everything | a subset | everything except dynamic features | Rails apps, Roda + Sequel started |
| My program | ran unchanged | three changes | ran unchanged | — |
| Download | ~8.6 MB compressed | ~0.9 MB compressed | 322 KB gzipped (whole program) | — |
| Runs in | browser, Node.js, WASI | browser | native, WASI | CLI, browser IDE |
These four are not really competitors. ruby.wasm brings Ruby’s full compatibility to the browser. PicoRuby.wasm makes Ruby light enough to replace JavaScript on a page. Spinel shows that Ruby programs can be compiled like C programs, down to a WebAssembly module of a few hundred kilobytes. And Roundhouse shows how much a static analysis can know about a web app, starting with Rails. For a Ruby developer, 2026 is a very good year for WebAssembly.
The timings above come from my development machine, which was busy with other work at the time, so read them as rough impressions and not as a benchmark. For a proper comparison of startup times, sue445’s ruby-wasm-startup-benchmark measures ruby.wasm and PicoRuby.wasm in the browser.
Links
- ruby.wasm and its documentation
- PicoRuby and PicoRuby.wasm on npm
- Spinel and its WebAssembly notes
- Roundhouse, the browser IDE and Sam Ruby’s article Campfire Chats