Back to Client Stories posts

How We Cut a Client’s Ruby on Rails Application’s Load Time in Half

Nov 2022

At Bit Zesty, we’re always looking for ways to improve the performance of our clients’ Ruby on Rails applications. Recently, we had the opportunity to help a client optimise their Rails app, and we managed to reduce their site load time by half for some endpoints. In this blog post, we’ll discuss the optimisations we made to achieve these results.

The application was an e-commerce platform that handled high amounts of flash sales, so it came with its own challenges. Typically, you can cache many parts of your application, but in this case, we needed more than caching to display accurate stock levels. The application was already running on the largest server available (Heroku Dyno) and short of re-platforming to another hosting company, so there were no further gains to be made by increasing the server size.

The first step was to investigate where most of the time was being spent.

New Relic was already in place to instrument the Ruby on Rails application; however, while we could see which actions/URLs were slow, it took a lot of work to pinpoint the exact places in the code causing the issues.

Custom Instrumentation

Most APMs (Application Performance Monitoring tools) work out of the box, but some aren’t very clear (especially if you are doing a lot of complex tasks to render your frontend) regarding what is causing the slowdown. This is where custom instrumentation comes in to help to give that extra context. We decided to manually instrument some of the most important pieces of code using the new_relic_rpm Ruby gem. This gave us much more visibility into which parts of the code took the longest to run.

Optimising SQL queries

N+1 queries are a well-known performance issue in Ruby on Rails applications. N+1 queries occur when you make a database query for a resource and add additional queries for each associated resource. For example, if you have a list of products on your site, and each product has many variants and a different price list, you might make one query to get the list of products and then additional queries for each product to get its related data.

There are a few ways to address N+1 queries, but the simplest is to use Ruby on Rails’ built-in .includes method. This allows you to specify which records should be eagerly loaded, so they are retrieved in a single database query.

In our case, the users could create their own store templates using liquid; this posed a problem that we did not have control over which relations to load for each view. We dynamically checked which models were being rendered, then for each model, used ActiveRecord::Associations::Preloader to preload the appropriate relations.

For most apps, though, using the .includes method would suffice to drastically reduce the number of SQL queries made. This resulted in a significant performance improvement.

Caching

While the app already used Memcache to cache parts of the site that could be cached, we noticed that the cache hit rate was rather low.

 As their user base grew, it put more pressure on the cache. What was initially sufficient for the amount of traffic the site used to receive, the cache now needed to be increased. 

Increasing the cache size meant more requests hit the cache, improving the hit rate. This is because the keys were not evicted too early. This improved the performance as the app didn’t have to make database queries or run complex code.

Image processing

The application handles many images, and most Rails apps use ImageMagick as their image-resizing library. While ImageMagick is a great tool, we found that a new default image processing library, Vips, is being introduced into Rails 7. Vips is much faster than ImageMagick for image processing, and by switching to it, we reduced the memory use and the time spent on image processing.

Jmalloc

Depending on your app’s memory use, switching from the standard Ruby memory allocator malloc to Jmalloc can improve performance. In our case, switching to Jmalloc reduced the amount of time spent on garbage collecting and helped us improve the app’s performance while reducing the RAM use. You can read more about malloc on Nate Berkopec’s blog.

Upgrading the Ruby on Rails version

The application was running the Ruby on Rails version 6.0, and several performance improvements have been made in Ruby on Rails since then. We upgraded the application to Ruby on Rails 6.1 and saw a significant performance improvement. Specifically, how image variants were cached in the CDN (content delivery network). While a Rails upgrade is usually the last item on the backlog, you should aim to keep it relatively up-to-date to benefit from the performance and security fixes.

Conclusion

While there is always room for further optimisation, these are just some of the steps we took to improve the performance of our client’s Ruby on Rails application. By taking a closer look at where most of the time was being spent and optimising the queries, we dramatically reduced the load on the database and made the site load faster.

If you’re interested in improving the performance of your Ruby on Rails application or just need some help upgrading and maintaining your app, get in touch with us today. We’d be happy to help!


Stay up to date with our blog posts

Keep up-to-date with our latest blog posts, news, case studies, and thought leadership.

  • This field is for validation purposes and should be left unchanged.

We’ll never share your information with anyone else. Read our Privacy Policy.