Unnamed Skill
This skill should be used when the user asks about Rails performance optimization, slow queries, caching strategies, database indexing, N+1 query prevention, memory optimization, asset optimization, profiling, benchmarking, Rack Mini Profiler, Bullet gem, query analysis, Russian doll caching, fragment caching, page speed, or production performance tuning. Also use when discussing YJIT, Puma configuration, or scalability. Examples:
$ 설치
git clone https://github.com/sjnims/rails-expert /tmp/rails-expert && cp -r /tmp/rails-expert/plugins/rails-expert/skills/performance-optimization ~/.claude/skills/rails-expert// tip: Run this command in your terminal to install the skill
name: performance-optimization description: This skill should be used when the user asks about Rails performance optimization, slow queries, caching strategies, database indexing, N+1 query prevention, memory optimization, asset optimization, profiling, benchmarking, Rack Mini Profiler, Bullet gem, query analysis, Russian doll caching, fragment caching, page speed, or production performance tuning. Also use when discussing YJIT, Puma configuration, or scalability. Examples:
Performance & Optimization: Rails at Scale
Overview
Rails applications can handle massive scale with proper optimization. Performance tuning involves multiple layers:
- Database optimization: Queries, indexes, eager loading
- Caching: Fragment, query, and low-level caching
- Asset optimization: Compression, CDN, HTTP/2
- Application server: Puma threads and workers
- Ruby optimization: YJIT, memory allocators
- Profiling: Identifying bottlenecks
Rails 8 provides excellent performance out of the box with Solid Cache, Thruster, and modern defaults.
Database Performance
N+1 Query Prevention
The #1 performance problem in Rails apps:
Problem:
# 1 query for products + N queries for categories
products = Product.limit(10)
products.each { |p| puts p.category.name } # N additional queries!
Solution:
# 2 queries total
products = Product.includes(:category).limit(10)
products.each { |p| puts p.category.name } # No additional queries
Use the Bullet gem in development to detect N+1:
# Gemfile
gem 'bullet', group: :development
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.console = true
end
Strict Loading
Enforce eager loading by raising errors when associations are lazily loaded:
# On a relation - raises ActiveRecord::StrictLoadingViolationError
user = User.strict_loading.first
user.address.city # raises error - not eager loaded
# On a record
user = User.first
user.strict_loading!
user.comments.to_a # raises error
# N+1 only mode - allows singular associations, catches collection lazy loads
user.strict_loading!(mode: :n_plus_one_only)
user.address.city # allowed (singular)
user.comments.first.likes.to_a # raises error (N+1 risk)
# On an association
class Author < ApplicationRecord
has_many :books, strict_loading: true
end
App-wide configuration:
# config/application.rb
config.active_record.strict_loading_by_default = true
# Log instead of raise
config.active_record.action_on_strict_loading_violation = :log
Use strict_loading in development/staging to catch N+1 queries before production.
Database Indexes
Add indexes for frequently queried columns:
# Migration
add_index :products, :sku
add_index :products, [:category_id, :available]
add_index :products, :name, unique: true
When to index:
- Foreign keys (category_id, user_id)
- WHERE clause columns
- ORDER BY columns
- JOIN conditions
- Unique constraints
Check with EXPLAIN:
Product.where(category_id: 5).explain
# Look for "Index Scan" (good) vs "Seq Scan" (bad)
Query Optimization
# Select only needed columns
Product.select(:id, :name, :price)
# Use pluck for single values
Product.pluck(:name) # Returns array of names
# Count efficiently
Product.count # COUNT(*) query
Product.size # Smart: uses count or length based on context
# Check existence
Product.exists?(name: "Widget") # Fast
# Batch processing
Product.find_each { |p| process(p) } # Loads in batches
Caching Strategies
Rails 8 uses Solid Cache by default (database-backed).
Fragment Caching
Cache rendered view fragments:
<% cache @product do %>
<%= render @product %>
<% end %>
Cache key includes:
- Model name and ID
updated_attimestamp- Template digest (auto-expires when view changes)
Collection Caching
Cache multiple items efficiently:
<%= render partial: 'products/product', collection: @products, cached: true %>
Reads all caches in one query, much faster than individual caching.
Russian Doll Caching
Nest caches that invalidate properly:
class Product < ApplicationRecord
belongs_to :category, touch: true # Update category when product changes
end
<% cache @category do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
When product updates:
- Product cache expires (updated_at changed)
- Category cache expires (touched via belongs_to)
- Other product caches reused
Low-Level Caching
# Cache expensive calculations
def complex_stats
Rails.cache.fetch("product_#{id}/stats", expires_in: 1.hour) do
calculate_expensive_statistics
end
end
# Cache with dependencies
Rails.cache.fetch(["product", id, :reviews, last_review_at]) do
reviews.includes(:user).order(created_at: :desc).limit(10)
end
SQL Query Caching
Rails automatically caches identical queries within a request:
Product.find(1) # Fires query
Product.find(1) # Uses cache (within same request)
Asset Optimization
Propshaft + Thruster
Rails 8's asset pipeline:
Propshaft handles:
- Digest fingerprinting (cache busting)
- Import map generation
- Asset precompilation
Thruster handles:
- Static file serving
- Gzip/Brotli compression
- Immutable caching headers
- X-Sendfile acceleration
CDN Integration
# config/environments/production.rb
config.asset_host = 'https://cdn.example.com'
Serves assets from CDN for faster global delivery.
Image Optimization
# Use Active Storage variants
<%= image_tag @product.image.variant(resize_to_limit: [800, 600]) %>
# Or ImageProcessing gem
<%= image_tag @product.image.variant(resize_and_pad: [800, 600, background: "white"]) %>
Application Server Optimization
Puma Configuration
# config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
Threads: Handle concurrent requests (5 is good default) Workers: Separate processes for parallelism (1 per CPU core)
Rules of thumb:
- More threads = better throughput, slightly higher latency
- More workers = true parallelism, more memory
- Start with: 2 workers, 5 threads per worker
YJIT (Just-In-Time Compiler)
Rails 8 enables YJIT by default (Ruby 3.3+):
# config/application.rb
config.yjit = true # Enabled by default in Rails 8
YJIT benefits:
- 15-30% faster execution
- Slightly higher memory usage
- Worth it for almost all apps
Memory Allocators
Use jemalloc for better memory management:
# Dockerfile
RUN apt-get install -y libjemalloc2
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
Reduces memory fragmentation with threaded servers.
Profiling Tools
Rack Mini Profiler
# Gemfile
gem 'rack-mini-profiler'
Shows in-page:
- SQL queries and duration
- View rendering time
- Partial rendering breakdown
- Memory usage
- N+1 warnings
Appears in top-left corner of every page in development.
Bullet (N+1 Detection)
# Gemfile
gem 'bullet', group: :development
# Detects:
# - N+1 queries (missing includes)
# - Unused eager loading
# - Unnecessary counter cache
Scout APM / Skylight
Production performance monitoring:
- Endpoint response times
- Slow query tracking
- N+1 detection in production
- Memory usage trends
- Error tracking
Performance Best Practices
Database
- Eager load associations with
includes - Add indexes on foreign keys and WHERE columns
- Use select to limit loaded columns
- Use pluck for extracting values
- Batch process with
find_each - Avoid COUNT queries when possible
- Use EXISTS for existence checks
- Profile queries with EXPLAIN
Caching
- Cache expensive operations with
Rails.cache.fetch - Use fragment caching for views
- Implement Russian doll caching for nested content
- Use Solid Cache (Rails 8 default)
- Cache API responses from external services
- Set appropriate expiry times
- Use cache sweepers sparingly
- Monitor cache hit rates
Code
- Avoid N+1 queries (use Bullet)
- Keep actions thin (fat models, skinny controllers)
- Use background jobs for slow operations
- Optimize Ruby code (avoid unnecessary allocations)
- Use YJIT (enabled by default Rails 8)
- Profile regularly (Rack Mini Profiler)
- Monitor production (APM tools)
- Load test before major releases
Assets
- Use CDN for static assets
- Enable compression (Thruster handles this)
- Optimize images (Active Storage variants)
- Use modern formats (WebP for images)
- Lazy load below-the-fold content
- Minimize JavaScript (Hotwire over heavy frameworks)
- Use HTTP/2 (Thruster supports this)
- Cache immutable assets forever
Measuring Performance
Benchmarking
require 'benchmark'
# Compare implementations
Benchmark.bm do |x|
x.report("approach 1:") { 1000.times { slow_method } }
x.report("approach 2:") { 1000.times { fast_method } }
end
Load Testing
# Apache Bench
ab -n 1000 -c 10 https://myapp.com/products
# wrk
wrk -t12 -c400 -d30s https://myapp.com/products
New Relic / Scout / Skylight
Production APM provides:
- Response time distributions
- Slow endpoint identification
- Database query analysis
- External API latency
- Error rates and patterns
Common Performance Issues
Symptom: Slow Page Load
Causes:
- N+1 queries
- Missing indexes
- Large result sets
- Expensive view rendering
- Missing fragment caching
Solutions:
- Profile with Rack Mini Profiler
- Check for N+1 with Bullet
- Add eager loading (
includes) - Add indexes
- Implement caching
Symptom: High Memory Usage
Causes:
- Memory leaks
- Large object allocations
- Inefficient garbage collection
- Too many Puma workers
Solutions:
- Use jemalloc allocator
- Reduce Puma workers
- Profile with
memory_profilergem - Find memory leaks with
derailed_benchmarks
Symptom: High Database Load
Causes:
- Missing indexes
- Inefficient queries
- N+1 problems
- Missing caching
Solutions:
- Add indexes on foreign keys
- Use
includesfor associations - Implement query caching
- Use database connection pooling
- Consider read replicas
Further Reading
For deeper exploration:
references/caching-guide.md: Complete caching strategies guidereferences/profiling-tools.md: How to profile and debug performance
For code examples:
examples/optimization-patterns.rb: Common optimization patterns
Summary
Rails performance involves:
- Database optimization: Indexes, eager loading, query efficiency
- Caching: Fragment, low-level, query caching (Solid Cache)
- Asset optimization: Propshaft, Thruster, CDN
- Server tuning: Puma configuration, YJIT
- Profiling: Finding bottlenecks before guessing
- Monitoring: Production performance tracking
Master these techniques and your Rails app will scale to millions of users.
Repository
