Exploring fast interop between JavaScript and Ruby

JavaScript, meet Ruby

While working on universal_renderer, I’ve been interested in different approaches for fast and stable interop between Ruby and JavaScript.

Ideally, we would be able to just server-side render some JavaScript within Ruby, but in practice it’s difficult due to high latency and low throughput. Instead, we have to make an HTTP request to a server running JavaScript, which means we have to wait until it returns before finally returning to the client.

While we could just re-architect our app to where our JavaScript client has it’s own server and Rails is simply API-only, but then we lose out on all kinds of benefits in security, tracking, among other things.

How do some other projects achieve this? Inertia Rails also provides a simple HTTP server with Express. This approach works great because the server is a long-running process that can be managed on it’s own, scaling up and down the number of workers in your deployment or isolating any risks of memory leaks.

Vite Ruby, on the otherhand, uses Open3 to pipe input and output to and from Vite via the shell. Though this works great for Vite, which is only called in development and at build time, it works poorly when it comes to server-side rendering contexts. Booting up Node or Bun just takes too long, and this kills throughput. It’s simply not scalable, even at very low loads.

So what are we to do? Well, universal_renderer just ships with a small NPM package to easily built an HTTP server with Express for you, similar to Inertia Rails. But how can we get this even faster?

One approach I’ve been experimenting with lately is mini_racer, which is an amazing gem that embeds a minimal implementation of the V8 engine (the same engine used in Chrome). It’s incredibly fast with extremely high throughput, low latency, and even a lower memory footprint. We get around the slow boot-up and high latency associated with embedded runtime approaches by keeping a connection pool of mini_racer instances, so they’re always hot and ready to go. The results speak for themselves:

Throughput Comparison Memory Usage Comparison

So why aren’t we using it?

Well, mini_racer is just V8. It’s not a full runtime like Node or Bun, which implement several different APIs on top to achieve all kinds of features like file system access, text encoding, and more. And unfortunately, many libraries like React DOM, React Query, and React Router expect some of these APIs to be available in the context of SSR, i.e. they assume that they’re running Node (which is a completely sane assumption if you’re not doing wacky stuff like trying to SSR JavaScript in Ruby).

Cloudflare similarly had this problem with Cloudflare workers, which do not run Node. The Cloudflare team added polyfills for missing Node APIs, which are just JavaScript implementations of missing features built for enviornments that don’t support it. Polyfills are typically used in the context of trying to write modern code while still getting it to run on older browsers. This was especially important when support for Internet Explorer was still something people worried about.

Adding polyfills to a stripped down JavaScript environment like V8 isn’t trivial because polyfills are often missing all of the little features needed to do things like server-side rendering. You end up having to work around these exceptions.

Oddly enough React’s renderToString actually has a browser-land implementation, which is great for using it with mini_racer, but other libraries (React Query, React Router) expect Node API’s to be available when running server-side only code.

I’m currently figuring out a way to get the right polyfills in place to support server-side functionality in all kinds of JavaScript libraries. If successful, it should allow for a much faster, more streamlined approach for rendering JavaScript apps within Ruby. The caveat is whether the overhead of the polyfills will negate any peformance advantage of mini_racer.

So that’s the current state, I’ve been hacking around with mini_racer to duct tape polyfills without needing to host an entire additional web server. I’ll be writing updates along the way, so stay tuned. Thanks for reading!