performance-optimization
npx machina-cli add skill aehkyrr/rails-expert/performance-optimization --openclawPerformance & 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.
Source
git clone https://github.com/aehkyrr/rails-expert/blob/main/plugins/rails-expert/skills/performance-optimization/SKILL.mdView on GitHub Overview
Optimizes Rails apps across database, caching, assets, and server configurations to deliver scalable performance. It covered profiling with Rack Mini Profiler and Bullet, preventing N+1 queries, leveraging caching (fragment, Russian doll, Solid Cache), and tuning Puma/YJIT for production.
How This Skill Works
Start by profiling with Rack Mini Profiler or Bullet to identify bottlenecks. Then apply targeted optimizations: fix slow queries with includes and strict_loading, add appropriate indexes, implement caching strategies (fragment, Russian doll, Solid Cache), optimize assets, and tune the application server (Puma threads/workers, YJIT). Re-run benchmarks to verify gains.
When to Use It
- When Rails queries are slow due to N+1 or missing indexes
- When you need a caching strategy for a product catalog or dashboard
- When profiling reveals memory pressure or asset load bottlenecks
- When configuring production server: Puma threads/workers and YJIT
- When aiming to improve page speed and overall production performance
Quick Start
- Step 1: Profile with Rack Mini Profiler or Bullet to identify hot paths
- Step 2: Apply optimizations (includes, strict_loading, indexing, caching, Puma/YJIT tuning)
- Step 3: Re-run profiling/benchmarking to confirm improvements
Best Practices
- Profile before and after changes with Rack Mini Profiler or Bullet
- Use eager loading (includes) and strict_loading to prevent N+1
- Add indexes on foreign keys and frequently queried columns
- Leverage Rails 8 caching features: fragment, Russian doll, and Solid Cache
- Benchmark and monitor memory usage, GC, and response times
Example Use Cases
- Product listing page slows due to N+1; fixed by eager loading with includes
- Bullet gem enabled in development to detect N+1 queries early
- Solid Cache implemented in Rails 8 to cache fragments and improve page speed
- Puma tuned with appropriate threads/workers and YJIT enabled in production
- Indexes added for category_id and name to speed up common queries