YJIT gives real-world Rails apps large speedups for free, and profiling tells you where the rest of the time goes.
YJIT is CRuby's just-in-time compiler, written in Rust by Shopify and production-ready since Ruby 3.2. It compiles hot methods to machine code using lazy basic block versioning: it generates code for a block only when it runs, specialised for the types actually seen. Typical Rails apps see 15 to 30 percent lower latency.
Enable it with ruby --yjit, the env var RUBY_YJIT_ENABLE=1, or at runtime with RubyVM::YJIT.enable (3.3+). Rails 7.2+ turns it on automatically in a generated initializer when available. Check with RubyVM::YJIT.enabled?, and read RubyVM::YJIT.runtime_stats when running with --yjit-stats. It costs some extra memory for compiled code; --yjit-mem-size caps it.
Code that YJIT, and the interpreter, handle well is monomorphic: call sites that see the same classes and object shapes every time. Initialise all instance variables in initialize, avoid redefining methods at runtime, and keep hot paths free of method_missing and heavy send.
Measure before changing anything:
benchmark-ipsreports iterations per second with error bars and warms up first, which matters with a JIT.- Sampling profilers such as
stackprofandvernier(which also shows GVL and GC time) find the slow code in a real workload, andrbspyattaches to a running process. - Often the real cost is in SQL queries, N+1 loads or allocations, not Ruby execution.
Keep Ruby updated: each release brings substantial YJIT and GC improvements.
Example
# Run with: ruby --yjit bench.rb (and compare with plain `ruby bench.rb`)
require "benchmark"
def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
puts "YJIT enabled? #{!!(defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?)}"
fib(20) # warm up so hot code gets compiled
t = Benchmark.realtime { fib(27) }
puts "fib(27): #{t.round(3)}s"
# Typical: interpreter ~0.20s, YJIT ~0.05s (varies by machine)
# For real comparisons use benchmark-ips:
# require "benchmark/ips"
# Benchmark.ips do |x|
# x.report("map.sum") { arr.map { it * 2 }.sum }
# x.report("sum") { arr.sum { it * 2 } }
# x.compare!
# endRemember
- Enable YJIT with --yjit, RUBY_YJIT_ENABLE=1 or RubyVM::YJIT.enable
- Monomorphic call sites and stable object shapes JIT best
- Profile with stackprof or vernier and benchmark with benchmark-ips
Watch out for
- Benchmarking without warm-up, so JIT compilation time pollutes the measurement.