skills/performance-optimization/SKILL.md
Identifies and fixes Rails performance issues including N+1 queries, slow queries, and memory problems. Use when optimizing queries, fixing N+1 issues, improving response times, or when user mentions performance, slow, optimization, or Bullet gem.
npx skillsauth add fernandezbaptiste/rails_ai_agents performance-optimizationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Performance optimization focuses on:
# Gemfile
group :development, :test do
gem 'bullet' # N+1 detection
gem 'rack-mini-profiler' # Request profiling
gem 'memory_profiler' # Memory analysis
end
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true
Bullet.add_footer = true
# Raise errors in test
# Bullet.raise = true
end
# config/environments/test.rb
config.after_initialize do
Bullet.enable = true
Bullet.raise = true # Fail tests on N+1
end
# BAD: N+1 query - 1 query for events, N queries for venues
@events = Event.all
@events.each do |event|
puts event.venue.name # Query per event!
end
# Generated SQL:
# SELECT * FROM events
# SELECT * FROM venues WHERE id = 1
# SELECT * FROM venues WHERE id = 2
# SELECT * FROM venues WHERE id = 3
# ... (N more queries)
# GOOD: Eager loading - 2 queries total
@events = Event.includes(:venue)
@events.each do |event|
puts event.venue.name # No additional query
end
# Generated SQL:
# SELECT * FROM events
# SELECT * FROM venues WHERE id IN (1, 2, 3, ...)
# Single association
Event.includes(:venue)
# Multiple associations
Event.includes(:venue, :organizer)
# Nested associations
Event.includes(venue: :address)
Event.includes(vendors: { category: :parent })
# Deep nesting
Event.includes(
:venue,
:organizer,
vendors: [:category, :reviews],
comments: :user
)
# preload: Separate queries (default for includes)
Event.preload(:venue)
# SELECT * FROM events
# SELECT * FROM venues WHERE id IN (...)
# eager_load: Single LEFT JOIN query
Event.eager_load(:venue)
# SELECT events.*, venues.* FROM events LEFT JOIN venues ON ...
# includes chooses automatically based on conditions
Event.includes(:venue).where(venues: { city: 'Paris' })
# Uses LEFT JOIN because of WHERE condition on venue
| Method | Use When |
|--------|----------|
| includes | Most cases (Rails chooses best strategy) |
| preload | Forcing separate queries, large datasets |
| eager_load | Filtering on association, need single query |
| joins | Only need to filter, don't need association data |
# app/models/event.rb
class Event < ApplicationRecord
scope :with_details, -> {
includes(:venue, :organizer, vendors: :category)
}
scope :with_stats, -> {
select("events.*,
(SELECT COUNT(*) FROM comments WHERE comments.event_id = events.id) as comments_count,
(SELECT COUNT(*) FROM event_vendors WHERE event_vendors.event_id = events.id) as vendors_count")
}
end
# Controller
@events = Event.with_details.where(account: current_account)
# Migration
add_column :events, :comments_count, :integer, default: 0, null: false
add_column :events, :vendors_count, :integer, default: 0, null: false
# Model
class Comment < ApplicationRecord
belongs_to :event, counter_cache: true
end
class EventVendor < ApplicationRecord
belongs_to :event, counter_cache: :vendors_count
end
# Usage - no query needed
event.comments_count
event.vendors_count
# BAD: Loads all columns
User.all.map(&:name)
# GOOD: Loads only name
User.pluck(:name)
# GOOD: For objects with limited columns
User.select(:id, :name, :email).map { |u| "#{u.name} <#{u.email}>" }
# BAD: Loads all records into memory
Event.all.each { |e| process(e) }
# GOOD: Processes in batches
Event.find_each(batch_size: 500) { |e| process(e) }
# GOOD: For updates
Event.in_batches(of: 1000) do |batch|
batch.update_all(status: :archived)
end
# BAD: Loads all records
if Event.where(status: :active).any?
if Event.where(status: :active).present?
# GOOD: SELECT 1 LIMIT 1
if Event.where(status: :active).exists?
# GOOD: For checking count
if Event.where(status: :active).count > 0
# count: Always queries database
events.count # SELECT COUNT(*) FROM events
# size: Uses counter cache or count
events.size # Uses cached value if available
# length: Uses loaded collection or loads all
events.length # Loads all records if not loaded
# Best practices:
events.loaded? ? events.length : events.count
# OR just use size (handles both cases)
# Check for missing foreign key indexes
ActiveRecord::Base.connection.tables.each do |table|
columns = ActiveRecord::Base.connection.columns(table)
fk_columns = columns.select { |c| c.name.end_with?('_id') }
indexes = ActiveRecord::Base.connection.indexes(table)
fk_columns.each do |col|
indexed = indexes.any? { |idx| idx.columns.include?(col.name) }
puts "Missing index: #{table}.#{col.name}" unless indexed
end
end
# Single column index
add_index :events, :status
# Composite index (order matters!)
add_index :events, [:account_id, :status]
# Unique index
add_index :users, :email, unique: true
# Partial index
add_index :events, :event_date, where: "status = 0"
# Covering index (PostgreSQL)
add_index :events, [:account_id, :status], include: [:name, :event_date]
| Add Index For | Example |
|--------------|---------|
| Foreign keys | account_id, user_id |
| Columns in WHERE | WHERE status = 'active' |
| Columns in ORDER BY | ORDER BY created_at DESC |
| Columns in JOIN | JOIN ON events.venue_id |
| Unique constraints | email, uuid |
# In console or specs
require 'memory_profiler'
report = MemoryProfiler.report do
# Code to profile
Event.includes(:venue, :vendors).to_a
end
report.pretty_print
# BAD: Loads all records
Event.all.map(&:name).join(', ')
# GOOD: Streams results
Event.pluck(:name).join(', ')
# BAD: Builds large array
results = []
Event.find_each { |e| results << e.name }
# GOOD: Uses Enumerator
Event.find_each.map(&:name)
# BAD: Instantiates all AR objects
Event.all.each do |event|
event.update!(processed: true)
end
# GOOD: Direct SQL update
Event.update_all(processed: true)
# GOOD: Batched updates
Event.in_batches.update_all(processed: true)
# Analyze query plan
Event.where(status: :active).explain
# Analyze with format
Event.where(status: :active).explain(:analyze)
# config/environments/production.rb
config.active_record.warn_on_records_fetched_greater_than = 1000
# Custom slow query logging
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
if event.duration > 100 # ms
Rails.logger.warn("SLOW QUERY (#{event.duration.round}ms): #{event.payload[:sql]}")
end
end
# spec/rails_helper.rb
RSpec.configure do |config|
config.before(:each) do
Bullet.start_request
end
config.after(:each) do
Bullet.perform_out_of_channel_notifications if Bullet.notification?
Bullet.end_request
end
end
# spec/requests/events_spec.rb
RSpec.describe "Events", type: :request do
it "loads index without N+1" do
create_list(:event, 5, :with_venue, :with_vendors)
expect {
get events_path
}.not_to raise_error # Bullet raises on N+1
end
end
# spec/support/query_counter.rb
module QueryCounter
def count_queries(&block)
count = 0
counter = ->(*, _) { count += 1 }
ActiveSupport::Notifications.subscribed(counter, "sql.active_record", &block)
count
end
end
RSpec.configure do |config|
config.include QueryCounter
end
# Usage
it "makes minimal queries" do
events = create_list(:event, 5, :with_venue)
query_count = count_queries do
Event.with_details.map { |e| e.venue.name }
end
expect(query_count).to eq(2) # events + venues
end
# Gemfile
gem 'rack-mini-profiler'
gem 'stackprof' # For flamegraphs
# config/initializers/rack_profiler.rb
if Rails.env.development?
Rack::MiniProfiler.config.position = 'bottom-right'
Rack::MiniProfiler.config.start_hidden = false
end
?pp=flamegraph for flamegraph?pp=help for all options# app/controllers/application_controller.rb
around_action :log_query_count, if: -> { Rails.env.development? }
private
def log_query_count
count = 0
counter = ->(*, _) { count += 1 }
ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
yield
end
Rails.logger.info "QUERIES: #{count} for #{request.path}"
end
| Problem | Solution |
|---------|----------|
| N+1 on belongs_to | includes(:association) |
| N+1 on has_many | includes(:association) |
| Slow COUNT | Add counter_cache |
| Loading all columns | Use select or pluck |
| Large dataset iteration | Use find_each |
| Missing index on FK | Add index on *_id columns |
| Slow WHERE clause | Add index on filtered column |
| Loading unused associations | Remove from includes |
development
Creates ViewComponents for reusable UI elements with TDD. Use when building reusable UI components, extracting complex partials, creating cards/tables/badges/modals, or when user mentions ViewComponent, components, or reusable UI.
development
Guides Test-Driven Development workflow with Red-Green-Refactor cycle. Use when the user wants to implement a feature using TDD, write tests first, follow test-driven practices, or mentions red-green-refactor.
data-ai
Configures Solid Queue for background jobs in Rails 8. Use when setting up background processing, creating background jobs, configuring job queues, or migrating from Sidekiq to Solid Queue.
testing
Creates service objects following single-responsibility principle with comprehensive specs. Use when extracting business logic from controllers, creating complex operations, implementing interactors, or when user mentions service objects or POROs.