Design Philosophy
This page explains the design decisions behind Ruby2JS and how it compares to other approaches for running Ruby in the browser.
Table of Contents
Three Approaches to Ruby in the Browser
There are three main ways to run Ruby code in a web browser:
1. Opal (Runtime Compilation)
Opal compiles Ruby to JavaScript with a comprehensive runtime library. It modifies JavaScript’s built-in objects to match Ruby semantics—for example, making a[-1] return the last element of an array.
Pros:
- High Ruby compatibility
- Ruby semantics preserved (negative indexing, truthiness, etc.)
Cons:
- Large runtime required
- Output is harder to read
- Compatibility issues with some JavaScript frameworks
2. WebAssembly Ruby (ruby.wasm)
ruby.wasm runs a full Ruby interpreter compiled to WebAssembly. This is actual Ruby running in the browser, not transpiled code.
Pros:
- Full Ruby compatibility (it is Ruby)
- Access to Ruby standard library
- Can run existing Ruby code unmodified
Cons:
- Large download (~20-40MB depending on configuration)
- Slower startup (must initialize Ruby VM)
- JavaScript interop requires explicit bridging
- Not suitable for generating JavaScript libraries
3. Ruby2JS (Static Transpilation)
Ruby2JS takes a different approach: it performs static transformations at build time to produce idiomatic JavaScript. There’s no runtime—just the generated code.
Pros:
- Small output (~460KB for the transpiled converter, walker, and runtime)
- Readable, debuggable JavaScript output
- Works seamlessly with JavaScript frameworks
- Generated code runs at native JavaScript speed
Cons:
- Not all Ruby features translate (see Anti-Patterns)
- Some semantic differences (truthiness, negative indexing)
- Requires understanding of what translates and what doesn’t
Ruby2JS Design Decisions
Choose Your Level of Ruby Compatibility
By default, Ruby2JS accepts JavaScript semantics rather than fighting them:
a[-1] # Returns undefined in JS, not last element
0 || 1 # Returns 0 in Ruby, 1 in JS (0 is falsy in JS)
But you can opt into more Ruby-like behavior at multiple levels:
Filters transform Ruby methods to JavaScript equivalents at transpile time:
# With functions filter:
arr.first # => arr[0]
arr.empty? # => arr.length === 0
str.gsub(/a/, 'b') # => str.replace(/a/g, 'b')
Polyfills add Ruby methods to JavaScript prototypes at runtime:
# With polyfill filter:
arr.first # => arr.first (property, via Object.defineProperty)
arr.compact # => arr.compact (mutating, like Ruby)
Pragmas give line-level control for edge cases:
x ||= default # Pragma: ?? # Use nullish coalescing
hash.each { |k,v| } # Pragma: entries # Use Object.entries()
Options like or: :nullish and truthy: :ruby can change behavior globally.
This layered approach lets you choose the trade-offs appropriate for your project—from minimal transformation to comprehensive Ruby compatibility.
Static Over Dynamic
The real limitations come from Ruby2JS using static AST transformations rather than runtime modifications:
- Predictable output - The same Ruby always produces the same JavaScript
- No runtime overhead - Generated code runs at native speed
- Framework compatible - No conflicts with React, Vue, etc.
The cost is that Ruby’s dynamic features simply can’t be supported. There’s no way to statically transpile method_missing, define_method, or eval—these require a runtime that can intercept and handle arbitrary method calls. See Anti-Patterns for the full list.
Edge Cases
Extending Existing Classes
Both Ruby and JavaScript have open classes, but Ruby unifies syntax for defining and extending classes while JavaScript does not. To extend an existing class, prepend ++:
++class String
def blank?
strip.empty?
end
end
This tells Ruby2JS you’re extending an existing class rather than defining a new one.
Suffix Stripping
Ruby allows ? and ! in method names; JavaScript doesn’t. Ruby2JS strips these suffixes:
array.empty? # => array.empty
string.chomp! # => string.chomp
This can be useful for avoiding filter conflicts—if a filter maps each to forEach, you can use each! to bypass it.
Further Reading
- User’s Guide - How to write dual-target Ruby/JavaScript code
- Options - Configuration including
excludefor filter control - notimplemented_spec - Ruby features known to be unsupported