performance-optimization

Performance Optimization for Rails 8

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "performance-optimization" with this command: npx skills add thibautbaissac/rails_ai_agents/thibautbaissac-rails-ai-agents-performance-optimization

Performance Optimization for Rails 8

Overview

Performance optimization focuses on:

  • N+1 query detection and prevention

  • Query optimization

  • Memory management

  • Response time improvements

  • Database indexing

Quick Start

Gemfile

group :development, :test do gem 'bullet' # N+1 detection gem 'rack-mini-profiler' # Request profiling gem 'memory_profiler' # Memory analysis end

Bullet Configuration

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

N+1 Query Problems

The Problem

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)

The Solution

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, ...)

Eager Loading Methods

includes (Preferred)

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 vs eager_load

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

When to Use Each

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

Query Optimization Patterns

Pattern 1: Scoped Eager Loading

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)

Pattern 2: Counter Caches

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

Pattern 3: Select Only Needed Columns

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}>" }

Pattern 4: Batch Processing

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

Pattern 5: Exists? vs Any? vs Present?

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

Pattern 6: Size vs Count vs Length

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)

Database Indexing

Finding Missing Indexes

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

Index Types

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]

When to Add Indexes

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

Memory Optimization

Finding Memory Issues

In console or specs

require 'memory_profiler'

report = MemoryProfiler.report do

Code to profile

Event.includes(:venue, :vendors).to_a end

report.pretty_print

Memory-Efficient Patterns

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)

Avoiding Memory Bloat

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)

Query Analysis

EXPLAIN in Rails

Analyze query plan

Event.where(status: :active).explain

Analyze with format

Event.where(status: :active).explain(:analyze)

Logging Slow Queries

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

Testing for Performance

N+1 Detection in Specs

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

Query Count Assertions

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

Rack Mini Profiler

Setup

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

Usage

  • Visit any page - profiler badge shows in corner

  • Click badge to see detailed breakdown

  • Add ?pp=flamegraph for flamegraph

  • Add ?pp=help for all options

Performance Checklist

Before Deployment

  • Bullet enabled in development/test

  • No N+1 queries in critical paths

  • Foreign keys have indexes

  • Counter caches for frequent counts

  • Eager loading in controllers

  • Batch processing for large datasets

  • Query analysis for slow endpoints

Monitoring Queries

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

Quick Fixes Reference

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

hotwire-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

i18n-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

solid-queue-setup

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

viewcomponent-patterns

No summary provided by upstream source.

Repository SourceNeeds Review