Skip to content
← All posts
6 min read Dawid Skłodowski

Ruby 3.5 and the state of YJIT and ZJIT

Ruby's JIT story has quietly become one of the most interesting things in the language. With Ruby 3.5, YJIT is mature and a sensible default — and a new method-based JIT, ZJIT, is emerging alongside it. A year-end look at where Ruby's performance work stands and why having two JIT approaches is a healthy sign.

It’s Christmas, so there’s a new Ruby — 3.5 — and the thread I want to pull on at year’s end is the one that’s quietly become the most interesting ongoing story in the language: its JIT compilers. For years now I’ve covered YJIT’s steady march from experiment to mature production default. With Ruby 3.5 that march is essentially complete — YJIT is something you just turn on — and at the same time a new JIT, ZJIT, is emerging alongside it with a different design philosophy. Two serious JIT efforts in one language is unusual, and it’s worth understanding what each is, why both exist, and what the existence of two says about the health of Ruby’s performance work. This is the year-end state of the JIT.

Where YJIT stands now

Start with the incumbent, because its story is one of patient success. YJIT — Ruby’s just-in-time compiler, which compiles hot Ruby to machine code at runtime — has been the throughline of recent releases, getting faster, more memory-efficient, and more mature with each one. By Ruby 3.5 that trajectory has reached its natural conclusion: YJIT is a safe, sensible production default, not an experimental flag you weigh carefully. You turn it on, you measure your app, you take the speedup. The long project of making a JIT that’s reliably worth enabling for real workloads is, for YJIT, done.

It’s worth holding onto the honest mental model I’ve stressed before: YJIT speeds up CPU-bound Ruby execution — the time your code spends running Ruby instructions — and not the I/O your app spends much of its time waiting on. So the real-world gain still varies by workload: compute-heavy code sees a lot, an I/O-bound web app sees less (but still real, and free). That hasn’t changed; what’s changed is that YJIT is now mature enough that the question isn’t “should I risk it?” but simply “turn it on and measure.”

What YJIT actually does, briefly

To understand why a second JIT would even appear, it helps to know YJIT’s approach in one paragraph. YJIT is built on a technique called basic-block versioning — it compiles Ruby lazily, one small chunk of execution at a time, generating machine code for the specific paths the program actually takes as it runs. This makes it fast to warm up and good at handling Ruby’s dynamism, because it compiles what it actually sees rather than reasoning about the whole method up front. It’s a pragmatic, incremental approach, and it’s served Ruby extremely well. But it’s one approach with its own characteristics, and that’s the opening for a different one.

ZJIT: a different approach emerging

The new development is ZJIT, a next-generation JIT being developed (out of the same Shopify-led effort that gave Ruby YJIT) with a different compilation philosophy. Where YJIT does lazy, basic-block-at-a-time compilation, ZJIT is a method-based JIT built on a more traditional compiler architecture — it analyzes and compiles whole methods using an intermediate representation, the kind of design classical optimizing compilers use. The motivation is that a method-level, IR-based approach can enable optimizations that are harder to express in the basic-block model — seeing more of the code at once opens the door to analysis and transformations that block-at-a-time compilation can’t easily do.

A few honest framings, because it’s early:

  • ZJIT is new and emerging, not a finished replacement. It’s the next phase of research and engineering on Ruby JIT, appearing alongside YJIT rather than displacing it. Treat it as the direction of future work, not as today’s production tool.
  • It represents a bet on a different point in the design space. Method-based, IR-driven compilation is a well-trodden path in compiler design, and bringing it to Ruby is an attempt to capture optimizations the current approach leaves on the table. Whether and when it surpasses YJIT in practice is exactly what the coming years will tell.
  • The two can inform each other. Lessons from YJIT’s years in production feed into ZJIT’s design, and the existence of both lets the Ruby team explore the trade-offs between the approaches with real evidence rather than speculation.

Why two JITs is a healthy sign

It might look strange, even wasteful, for one language to have two serious JIT compilers — but I’d argue it’s the opposite: a sign of health. Consider what it actually indicates:

  • Sustained, serious investment in Ruby performance. A language gets one mature JIT and a next-generation one being actively developed only when there’s real, ongoing engineering commitment to making it fast. That investment is a vote of confidence in Ruby’s future as a performant language, not just a productive one.
  • Exploring the design space rather than settling. Having YJIT proven in production while ZJIT explores a different architecture means the Ruby team isn’t betting everything on one approach — they’re learning which trade-offs actually pay off, with a working fallback the whole time. That’s how you make progress on hard problems without risking the stable thing people depend on.
  • The performance story is far from over. A few years ago the question was whether Ruby would get a production-viable JIT at all. Now it has a mature one and an ambitious successor in development. The trajectory of Ruby getting steadily faster, release over release, clearly has more room to run.

This is the same maturity-without-stagnation quality I’ve admired in Ruby’s other recent work — careful, evidence-driven, willing to invest in a better foundation while keeping the proven one running. Two JITs isn’t duplication; it’s a language taking its own performance seriously enough to do the hard, patient work of improving it properly.

Verdict

Ruby’s JIT story has quietly become one of the most interesting things in the language, and Ruby 3.5 marks a milestone in it. YJIT has completed its long march from experiment to mature production default — you simply turn it on and measure, holding the honest model that it accelerates CPU-bound Ruby execution rather than the I/O your app waits on, so the gain is workload-dependent but real and free. And a new method-based JIT, ZJIT, is emerging alongside it, built on a more traditional IR-driven compiler architecture that aims to capture optimizations the basic-block approach can’t easily express — early, not a finished replacement, but a clear statement of where the work is heading. The existence of two serious JIT efforts in one language isn’t waste; it’s a sign of health — sustained investment in Ruby performance, a willingness to explore the design space with a proven fallback in place, and clear evidence that the trajectory of Ruby getting faster release over release is far from finished. After years of covering each Christmas release, the through-line is clear: Ruby is a mature language that refuses to stagnate, and its JIT story — one compiler proven, another being built to surpass it — is the sharpest example of that. Turn on YJIT today; watch ZJIT with interest, because it’s where Ruby’s performance is going next.