This episode covers Ruby performance optimization: enabling YJIT with the --yjit and --yjit-mem-size flags, measuring speed with Benchmark, profiling with ruby-prof, and understanding GC.stat, GC.config, and allocation hotspots to find the cause of slow applications.

A secure and correct application isn't enough — in production, speed determines server costs and user experience. Episode 17 covers Ruby performance optimization: enabling YJIT, measuring speed with Benchmark, profiling with ruby-prof, and understanding garbage collection.
Many slow applications aren't slow because Ruby is slow, but because the code allocates excessive objects or skips the JIT. We start with the easiest step with a big impact: turning on YJIT. Throughout the episode, always tie every decision to measured numbers.
YJIT is a production-ready Just-in-Time compiler for CRuby. Without changing code at all, you can get significant speedups just by adding a flag when running Ruby:
ruby --yjit app.rb
ruby --yjit --yjit-mem-size=64 app.rbruby --yjit app.rb enables the compiler inside the process. --yjit-mem-size limits the memory used by compiled code, 128 MiB by default. For Rails, enable it through the RUBY_YJIT_ENABLE=1 environment variable or in config/production.rb. Once enabled, confirm the process is actually using it with RubyVM::YJIT.enabled?.
YJIT provides internal statistics. Enable statistics with the RUBY_YJIT_STATS=1 environment variable, then read them from code:
puts RubyVM::YJIT.runtime_statsRubyVM::YJIT.runtime_stats returns a hash with metrics like the amount of compiled code, the number of side-exits, and the compilation ratio. Compare statistics before and after code optimization — focus on high side-exits because they indicate code that gets deoptimized frequently. Excessive side-exits usually come from methods that frequently change argument types.
Before optimizing, measure first. Benchmark compares implementations and gives numbers you can base decisions on:
require "benchmark"
time = Benchmark.realtime do
1_000_000.times { "ruby".upcase }
end
puts "waktu: #{time} detik"Benchmark.realtime returns the seconds the block spent. To compare several approaches at once, use Benchmark.bm, which prints a table per implementation:
require "benchmark"
Benchmark.bm do |x|
x.report("upcase") { 1_000_000.times { "ruby".upcase } }
x.report("capitalize") { 1_000_000.times { "ruby".capitalize } }
endDon't optimize without a baseline — measured numbers always beat intuition. Repeat measurements several times because the CPU can fluctuate, then take the median value.
Benchmark measures total time, but doesn't tell you which part is slow. That's where ruby-prof comes in: it profiles every method call and compiles a report:
gem install ruby-prof
ruby-prof -m --printer=call_stack app.rbruby-prof -m --printer=call_stack app.rb writes the profile in wall-time mode and prints the call stack. Look for methods with the largest self time — that's where allocations and computations pile up. ruby-prof produces various formats, including graph and flat for analysis, and can be integrated with Rails via rack middleware.
Ruby cleans up unused objects through garbage collection. Monitor its activity with GC.stat, and adjust its behavior with GC.config:
puts GC.stat[:count]
GC.config(major_gc_interval: 180)GC.stat[:count] shows how many times GC has run since the process started. GC.config(major_gc_interval: 180) sets the major GC interval in seconds — useful for reducing freezes in latency-sensitive applications. The RUBY_GC_HEAP_INIT_SLOTS and RUBY_GC_HEAP_GROWTH_FACTOR environment variables can also be set externally to control the heap growth rate.
Allocation hotspots are points where code creates excessive temporary objects — the cause of GC running too often. ObjectSpace helps measure:
puts ObjectSpace.count_objects[:T_STRING]ObjectSpace.count_objects[:T_STRING] counts the String objects currently alive. A drastic rise and fall indicates excessive allocation in a loop or in a request hot path. Common reduction patterns: avoid creating an array per iteration, use frozen literals, and move object creation outside loops. Also monitor the process's RSS growth over time to see memory leaks.
Warning
Optimization is the science of measurement. Without profiling, you're just guessing. Measure first with Benchmark and ruby-prof, change one thing at a time, then measure again.
Key takeaways:
--yjit and gives speedups without changing code.--yjit-mem-size limits compiled code memory, 128 MiB by default.RubyVM::YJIT.runtime_stats reveals sides that get deoptimized frequently.GC.stat and GC.config monitor and adjust GC behavior.In episode 18 we add precision: the Ruby type system with RBS and Steep for static type checking, and Sorbet with gradual typing and sorbet-typed. After performance, it's the turn of data-type reliability guaranteed by the compiler.