Introduction: Performance is a Full-Stack Problem

In a Rails application, performance bottlenecks can hide anywhere. A slow page might not be a single issue, but a combination of problems spanning the entire stack—from inefficient database queries and slow background jobs to complex view rendering. This case study details a holistic optimization effort on a Rails application, tackling two distinct problems: a long-running data import task and a slow-loading web page.

The results were dramatic: we cut the data import time from nearly a minute to just 18 seconds and slashed the page load time from over 2 seconds to a snappy 156 milliseconds.

Part 1: Optimizing a Slow Data Import Rake Task

The Problem: A Rake task designed to import a large JSON file into the database was taking too long to complete, exceeding our one-minute performance budget.

The Investigation: Using profilers like memory-profiler, we analyzed the task. The flame graph immediately pointed to a significant amount of time being spent in ActiveModel::AttributeMethods#method_missing. This is often a symptom of excessive ActiveRecord object instantiation in a tight loop. We also knew that inserting records one by one was causing too many database round-trips.

Flame graph showing a bottleneck in ActiveModel

Solution 1: Switch to Bulk Data Insertion

Instead of creating records one by one, which results in an INSERT statement for every row, we used the activerecord-import gem. This powerful utility allows you to insert thousands of records with a single SQL statement.

Solution 2: Avoid Unnecessary Object Instantiation

The method_missing bottleneck was caused by creating full ActiveRecord objects from our source data just to use their IDs in an association. The fix was to switch to using raw IDs, avoiding the massive overhead of object creation and initialization inside a loop.

The Result: Combining bulk inserts with reduced object allocation cut the import time for our large test file to a mere 18 seconds.

Part 2: Optimizing a Slow Page Load

The Problem: The main page for viewing bus routes was taking over 2,000ms to load, creating a poor user experience.

The Investigation: Using the bullet and rack-mini-profiler gems, we quickly identified the classic Rails performance killer: a series of N+1 queries.

Solution 1: The N+1 Killer - Using includes

The page needed to display a list of trips, along with the bus model for each trip and the services (like Wi-Fi or A/C) available on that bus. The original code was making one query to load the trips, and then for each trip, making another query to fetch its bus, and for each bus, making yet another query to fetch its services. This is an N+1 query storm.

  • The Fix: The solution is to tell ActiveRecord to load all the necessary associated data upfront using includes. This single change in the controller reduces hundreds of potential queries to just three.

  • The Code:

    # app/controllers/trips_controller.rb
    
    class TripsController < ApplicationController
      def index
        @from = City.find_by_name!(params[:from])
        @to = City.find_by_name!(params[:to])
    
        # Use includes to preload the bus and its services
        @trips = Trip.includes(bus: :services).where(from: @from, to: @to).order(:start_time)
      end
    end
    

Solution 2: The Database’s Best Friend - Indexing

Even with N+1s fixed, queries can be slow if the database has to perform full table scans. Tools like pghero can identify missing indexes. We found that the foreign key columns used in our queries (from_id, to_id, bus_id) were not indexed.

  • The Fix: We added a migration to create indexes on these columns. An index acts like a phonebook for your database, allowing it to find the data it needs almost instantly.

  • The Schema:

    # db/schema.rb
    
    create_table "trips", force: :cascade do |t|
      # ... other columns
      t.integer "bus_id"
      # This composite index helps find trips between two cities quickly
      t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to_id"
    end
    

    (Note: An index on bus_id would also be added in a real-world scenario)

Solution 3: Efficient View Rendering

The final bottleneck was in the view itself. Rendering many small, nested partials in a loop can add significant overhead. We refactored the view code into a single, streamlined template, which further reduced the rendering time.

The Result: By layering these three optimizations, the page load time plummeted from over 2000ms to a final, production-ready 156ms.

Conclusion: A Full-Stack Performance Checklist

This case study provides a repeatable checklist for tackling Rails performance issues:

  1. For Slow Jobs: Use bulk-insertion tools for mass data imports and avoid creating expensive objects in tight loops.
  2. For Slow Pages: Use bullet to hunt down and eliminate N+1 queries with includes.
  3. For Slow Queries: Ensure all foreign key columns and columns used in WHERE clauses are indexed.
  4. For Slow Renders: Simplify complex views and avoid excessive use of partials in loops.
  5. Always Profile: Use tools like rack-mini-profiler and memory_profiler to get concrete data. Don’t guess.