Rails Performance: 5 Mistakes That Slow Your App Down

Rails Performance

Rails promises rapid development. That is true that ruby application development allowes for quick building and deployment but it might come at the expense of performance problems down the line.

In this read, well look into crucial mistakes that might slow your ruby app and learn how to prevent them.

Neglecting Eager Loading

N+1 queries turn simple page loads into database nightmares. The application fetches a list of records, then queries the database separately for each associated record.

A homepage loading 50 blog posts makes this mistake:

  • First query fetches 50 posts,
  • Application loops through posts to display author names,
  • Rails executes 50 separate queries to load each author,
  • Total: 51 queries instead of 2.

Response times rises from 200ms to over 2 seconds. That’s a 10x performance hit from one missing method call. The Bullet gem detects these during development. I tracked a Flipper feature flag query executing 38 times on a single page.

Each query took 60 to 200ms:

Query Count Individual Time Total Impact
1 query (optimized) 150ms 150ms
38 queries (N+1) 60-200ms each ~6,000ms (6 seconds)

The fix involves calling includes() when fetching records. Rails loads all associated data in a second query using a SQL JOIN. Two queries handle what previously required dozens.

Fetching Every Column

ActiveRecord’s default behavior loads every column from the database. This creates memory bloat when tables contain large text or binary fields.

The standard User.all query pulls everything:

  • All columns, regardless of usage;
  • Large biography text fields;
  • JSON configuration blobs;
  • Binary profile images are stored in the database.

Fetching 1,000 user records with 12 columns allocates 1,723,042 bytes in memory. That’s 11,577 Ruby objects created to hold database results.

You might not notice this with 10 users in development. Production databases with 100,000 users crash the application. The view renders fine locally, but times out when deployed.

Using select() to specify needed columns cuts memory usage:

  • select(:id, :name, :email) loads three columns,
  • Ignores large text fields,
  • Reduces memory allocations by 60-80%,
  • Database reads fewer bytes from disk.

Database read IOPS become a pitfall. Fetching 12 columns instead of 3 means the database performs 4x more disk operations.

Missing Database Indexes

Queries without indexes force sequential scans. The database reads every row in the table to find matching records.

Some slow queries can be debugged by using indexes:

  • Query searches for users.email = ‘[email protected]’;
  • No index exists on the email column;
  • PostgreSQL scans all rows sequentially;
  • Adding an index transforms the query from hundreds of milliseconds to single-digit milliseconds.

The difference becomes extreme at scale. Sequential scans grow linearly with table size. A table with 1 million rows takes dramatically longer to scan than one with 50,000 rows. Indexed lookups maintain consistent speed regardless of table size.

Rails migrations make index creation straightforward. The challenge comes from knowing which columns need indexing. Any column used in WHERE clauses, JOIN conditions, or ORDER BY statements benefits from an index.

Common candidates include:

  • Foreign keys (user_id, post_id),
  • Email addresses for authentication,
  • Status fields filtered in queries,
  • Timestamps used for sorting.

Indexes consume disk space and slow down INSERT operations slightly. The trade-off favors query performance in production. Most applications read data far more often than they write it.

View Rendering Bloat

Two-thirds of memory allocations during page rendering come from the view layer, not ActiveRecord. Professionals often optimize database queries but leave views untouched.

Rendering 1,000 records versus 10 shows the difference:

  • 1,000 records trigger massive string allocations,
  • Each attribute converts to a string for HTML display,
  • Table rows and HTML elements multiply allocations,
  • View rendering allocates more memory than fetching the data.

Partials make this worse. Rendering a partial 100 times means Rails processes the template file 100 separate times. Each iteration creates new Ruby objects for the template context.

Collection rendering provides an alternative:

  • render partial: “post_card”, collection: @posts,
  • Rails optimizes the loop internally,
  • Reduces object allocations significantly,
  • Faster than manual iteration with render.

Queries inside views create hidden N+1 problems. The controller fetches posts, then the view checks post.comments.where(approved: true).any? for each post.

This pattern shows up repeatedly:

  • Controller loads data without considering view needs,
  • View executes additional queries during rendering,
  • Development with small datasets hides the problem,
  • Production traffic exposes the bottleneck.

Moving logic to the controller or model layer prevents this. Precompute values like has_approved_comments? before rendering. The view receives ready-to-display data without touching the database.

Compiling Assets During Deployment

Asset compilation blocks deployments. Production servers take 2 minutes and 50 seconds to compile JavaScript and CSS files.

The process involves multiple steps:

  • Boot Rails environment (20-30 seconds),
  • Load all gems and initializers,
  • Compile CoffeeScript to JavaScript,
  • Process SCSS to CSS,
  • Minify and fingerprint files.

Clearing the asset cache before each compilation adds time. One team measured compilation dropping from 2:50 to 29 seconds after disabling automatic cache clearing. The cache clearing exists to handle environment changes, but production deployments rarely modify compiler settings.

Switching from Webpacker to Esbuild or Bun provides another optimization:

  • Webpacker: 4.82 seconds for asset compilation,
  • Esbuild: 1.99 seconds (59% faster),
  • Bun: 1.95 seconds (60% faster).

These bundlers handle JavaScript compilation faster than Ruby-based tools. The difference grows with larger applications containing thousands of JavaScript files.

Precompiling assets locally offers a different approach. Developers compile assets on their machines, commit the compiled files, and deploy static assets. This eliminates compilation during deployment entirely but requires discipline around committing compiled files to version control.

Conclusion

N+1 queries, SELECT * bloat, missing indexes, view rendering waste, and slow asset compilation all stem from Rails optimizing for developer speed over runtime performance. Prioritize performance over fast feature rollout.

Charles Poole is a versatile professional with extensive experience in digital solutions, helping businesses enhance their online presence. He combines his expertise in multiple areas to provide comprehensive and impactful strategies. Beyond his technical prowess, Charles is also a skilled writer, delivering insightful articles on diverse business topics. His commitment to excellence and client success makes him a trusted advisor for businesses aiming to thrive in the digital world.

Leave a Reply

Your email address will not be published. Required fields are marked *

Close