Skip to content

Ruby Performance Optimization — Profiling Caching YJIT and Memory Management Explained

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Ruby Performance Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby Performance Optimization covers profiling with benchmark, stackprof, and rack-mini-profiler, Caching strategies with memoization and Redis, YJIT JIT Compilation, and memory management techniques.

What You'll Learn

  • Profiling Ruby code
  • Caching strategies
  • YJIT JIT compilation
  • Memory and GC optimization

Why It Matters

Performance affects user experience. Doda Browser optimizes page load times. Durga Antivirus Pro optimizes scan throughput. One well-placed cache or optimized query can 10x your application speed.

Real-World Use

Rails applications use fragment caching, Russian Doll caching, and query caching. Background Jobs optimize batch processing. APIs use HTTP caching. Web servers use connection pooling.

flowchart LR
    A["Performance"] --> B["Profiling"]
    B --> C["Caching"]
    C --> D["YJIT"]
    D --> E["GC Tuning"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Profiling with Benchmark

require "benchmark"

n = 1_000_000

Benchmark.bm do |x|
  x.report("string+")   { n.times { "foo" + "bar" } }
  x.report("interpol")  { n.times { "foo#{'bar'}" } }
  x.report("concat")    { n.times { "foo".concat("bar") } }
end

#                user     system      total        real
# string+    0.123456   0.000123   0.123579  (0.123789)
# interpol   0.098765   0.000098   0.098863  (0.098912)
# concat     0.087654   0.000087   0.087741  (0.087823)

Benchmark.bmbm

# Rehearsal to stabilize performance
Benchmark.bmbm do |x|
  x.report("fast") { fast_method }
  x.report("slow") { slow_method }
end

Profiling with StackProf

require "stackprof"

StackProf.run(mode: :cpu, out: "tmp/stackprof.dump") do
  # Code to profile
  1000.times { expensive_operation }
end

# Analyze with:
# stackprof tmp/stackprof.dump
# stackprof tmp/stackprof.dump --method expensive_method

Memory Profiling

require "memory_profiler"

report = MemoryProfiler.report do
  # Code to profile
  array = 1000.times.map { |i| "string #{i}" }
end

report.pretty_print
# Shows: allocated memory, retained memory, objects by location

Caching Strategies

Memoization

class ExpensiveService
  def compute(id)
    @cache ||= {}
    @cache[id] ||= begin
      puts "Computing for #{id}..."
      sleep(1)  # Simulate expensive operation
      id * 2
    end
  end
end

service = ExpensiveService.new
puts service.compute(5)  # Computing for 5... => 10
puts service.compute(5)  # => 10 (cached, no computation)

Redis Caching

require "redis"

class CacheStore
  def initialize
    @redis = Redis.new(url: ENV["REDIS_URL"])
  end

  def fetch(key, ttl: 300)
    cached = @redis.get(key)
    return JSON.parse(cached) if cached

    value = yield
    @redis.setex(key, ttl, value.to_json)
    value
  end

  def delete(key)
    @redis.del(key)
  end

  def clear_pattern(pattern)
    keys = @redis.keys(pattern)
    @redis.del(*keys) unless keys.empty?
  end
end

cache = CacheStore.new
result = cache.fetch("user:#{user_id}", ttl: 60) do
  User.find(user_id).expensive_query
end

Rails Caching

# Fragment cache
<% cache @article do %>
  <%= render @article %>
<% end %>

# Russian doll caching
<% cache [@article, @article.comments.maximum(:updated_at)] do %>
  <%= render @article %>
  <%= render @article.comments %>
<% end %>

# Low-level caching
Rails.cache.fetch("expensive_calc", expires_in: 1.hour) do
  expensive_calculation
end

# HTTP caching
class ArticlesController
  def show
    @article = Article.find(params[:id])
    fresh_when @article
  end
end

YJIT Optimization

# Enable YJIT (Ruby 3.1+)
# Add to your application:
# RUBYOPT="--yjit"
# Or in config:
# $ ruby --yjit myapp.rb

# YJIT stats
RubyVM::YJIT.runtime_stats  # Returns hash of JIT stats

# Example YJIT configuration
# --yjit-exec-mem=256   # Code cache size
# --yjit-call-threshold=30  # Calls before JIT compilation

Object Allocation Reduction

# Bad — creates many intermediate objects
def process(items)
  items.map { |i| i.to_s }
       .select { |s| s.length > 3 }
       .map(&:upcase)
end

# Better — single pass
def process(items)
  result = []
  items.each do |i|
    s = i.to_s
    result << s.upcase if s.length > 3
  end
  result
end

# Avoid creating unnecessary objects
# Bad: "Hello, #{name}!"  # Creates new string
# Good: "Hello, #{name}!" # Same — Ruby handles this efficiently

GC Tuning

# Check current GC settings
GC.stat
GC.latest_gc_info

# Tune GC
GC::Profiler.enable
# Run code...
GC::Profiler.report

# Major GC tuning
# RUBY_GC_HEAP_GROWTH_FACTOR=1.15
# RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=2.0
# RUBY_GC_MALLOC_LIMIT=67108864

# Manual GC
GC.start      # Force GC
GC.compact    # Compact heap (Ruby 2.7+)

Common Mistakes

1. Premature Optimization

# Bad — optimizing before measuring
def process(x)
  # Complex optimization
  cache[x] ||= begin
    result = x * 2
    result -= 1 if result > 100
    result
  end
end

# Good — measure first, optimize bottlenecks

2. Over-Caching

# Cache stampede when cache expires
# Multiple requests all compute the same value

# Solution: use Rails.cache.fetch with race_condition_ttl
Rails.cache.fetch("key", expires_in: 1.hour, race_condition_ttl: 10) do
  expensive_calculation
end

3. Not Using Indexes

# Bad — full table scan
User.where("name LIKE '%search%'")

# Good — use database indexes
add_index :users, :name

4. N+1 Queries

# Bad — queries for each article's comments
@articles.each { |a| a.comments.count }

# Good — use includes or counter_cache
@articles = Article.includes(:comments)

5. Creating Unnecessary Objects in Loops

# Bad — creates new array each iteration
items.each do |i|
  [i, i * 2, i * 3].each { |v| process(v) }
end

# Good — reuse
values = [nil, nil, nil]
items.each do |i|
  values[0] = i
  values[1] = i * 2
  values[2] = i * 3
  values.each { |v| process(v) }
end

Practice Questions

1. What is YJIT and how does it help?

YJIT (Yet Another Ruby JIT) compiles hot code paths to native machine code at runtime, improving execution speed by 2-5x for CPU-bound operations. Enable with --yjit.

2. What is memoization?

Caching the result of a method call so subsequent calls return the cached value without re-computing. Pattern: @cache ||= compute_value.

3. How do you find performance bottlenecks?

Use Benchmark for micro-benchmarks, StackProf for CPU profiling, rack-mini-profiler for Rails request profiling, and MemoryProfiler for memory analysis.

4. What is Russian doll caching?

Nested cache keys based on record timestamps. When a child record updates, only its parent's cache is invalidated. cache [article, article.comments.maximum(:updated_at)].

Challenge: Optimize a method that processes a CSV of user data, using profiling to identify and fix bottlenecks.

Solution
require "csv"
require "benchmark"

# Before optimization
def process_csv_slow(filepath)
  users = []
  CSV.foreach(filepath, headers: true) do |row|
    users << {
      name: row["name"].strip,
      email: row["email"].strip.downcase,
      age: row["age"].to_i
    }
  end
  users.select { |u| u[:age] >= 18 }
       .sort_by { |u| u[:name] }
end

# After optimization
def process_csv_fast(filepath)
  users = []
  CSV.foreach(filepath, headers: true) do |row|
    name = row["name"]
    email = row["email"]
    age = row["age"]

    next unless name && email && age

    name.strip!
    email = email.strip.downcase
    age = age.to_i

    users << { name: name, email: email, age: age } if age >= 18
  end
  users.sort_by! { |u| u[:name] }
  users
end

Benchmark.bm do |x|
  x.report("slow") { process_csv_slow("test.csv") }
  x.report("fast") { process_csv_fast("test.csv") }
end

FAQ

{{< faq question="Should I always enable YJIT?" >}} Yes for Ruby 3.3+. YJIT is production-ready and provides 2-5x speedup for most workloads. Enable with RUBYOPT="--yjit" or ruby --yjit your_app.rb. {{< /faq >}}

{{< faq question="How often should I profile my application?" >}} After every significant code change. Monitor production performance with APM tools (Scout, New Relic). Profile whenever you add a new feature that could be performance-sensitive. {{< /faq >}}

{{< faq question="What's the most impactful performance optimization?" >} Database query optimization (N+1, missing indexes). Then caching. Then Ruby-level optimizations (allocation, GC). Measure before optimizing. {{< /faq >}}

{{< faq question="How does Ruby's garbage collector work?" >}} Ruby uses a generational GC (since 2.1) with minor GC for young objects and major GC for old objects. GC.stat shows collection counts and timings. {{< /faq >}}

{{< faq question="Should I freeze strings for performance?" >}} frozen_string_literal: true (Ruby 3.0+) makes all string literals frozen by default, reducing allocations. Add it at the top of files: # frozen_string_literal: true. {{< /faq >}}

Try It Yourself

# performance_demo.rb
require "benchmark"

def slow_method(items)
  result = []
  items.each do |i|
    result << i.to_s.upcase + "!"
  end
  result
end

def fast_method(items)
  items.map! { |i| "#{i.to_s.upcase}!" }
end

data = (1..10000).to_a

Benchmark.bm do |x|
  x.report("slow") { slow_method(data) }
  x.report("fast") { fast_method(data.dup) }
end

# Compare memory
require "memory_profiler"
report = MemoryProfiler.report do
  fast_method((1..1000).to_a)
end
report.pretty_print

What's Next

Now that you understand performance optimization, learn about threading and concurrency in Ruby.

Topic Description Link
Ruby Threads Thread creation, synchronization {{< ref "40-threads" >}}
Ruby Ractors Parallel execution, Ractors {{< ref "42-ractors" >}}
Python Profiling Compare Python's profiling tools Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro