Fighting spree performance bottlenecks

By Amit Bisht

Application Performance problems, due to heavy traffic, are something every engineering team deals with when working on a production application. It is a known fact that if there is no performance problem, it means you are not growing enough (Time to re-think the strategy!). For the marketing team, the news is great because the traffic has increased but things are not good for the engineering department as the site is not able to provide adequate services to the user’s request which may result in huge losses with regard to retaining a customer or maintaining a monetary value, especially in e-commerce.

I recently dealt with a similar issue while working on an e-commerce application. It is build using Spree (A popular ruby on rails e-commerce framework) and React library. I will elaborate some of the techniques which were implemented to enhance the application performance under high traffic; resulting in a monthly sales of 800K to 1 million dollars (...People are happy now).

Eliminating n + 1 queries.

Let us look at the following piece of code.

product.variant_images.select do |image|

image.viewable.option_value('color') == color

end

The code runs again & again over the variant images of a product and selects only those viewable that have color option_value. So when we run,

product.variant_images

it queries the database and gets the data.

Let us assume 5 records. Then as it iterates over those records for a single variant_image, it again creates a database query when we run,

Assuming it creates two or more DB query when the above code is executed, for the whole process, there will be eleven DB queries performed (from 10 or 50 variant_images).

To solve it we can use active record’s own include keyword that showcases the record from the table on which relation exists.

When we included viewable in the initial query, the viewable data was fetched as an active record relation and further queries are performed. Now the Query count is reduced from 11 to 1.

How to find them?

https://github.com/flyerhzm/bullet

You can use above gem for you n + 1 search and destroy missions.

2. High-cost functions running from embedded ruby view.

Spree has a few functions majorly related to displaying price, sale price at the views which ran when the view loaded and did an insane amount of DB queries. To fix it, I used a hash map and fragment caching.

We can create a hash map of the product id(s) and the prices that are to be displayed on the view according to the application’s business logic. We can use Active Record to fetch prices but I would recommend using

ActiveRecord::Base.connection.exec_query(“”)

To run SQL and fetch the record, it enhances performance many folds. Then pass the map into the view through the action method but this alone is not useful, it is the fragment caching that does the magic. Add the hash map to fragment cache (which will be explained later) and all is well in the world again.

3. Add Fragment Caching

Although Rails has three types of caching but fragment caching is the real underdog as it is not utilized as much as it should be. This caching strategy allows us to cache at the lowest level possible in our code base.

Result = Rails.cache.fetch("#{@product.id}-#{@product.updated_at}/product_detail_without_sale_data/true") do

getVariants

End

Here the function getVariants queries the DB for all the variants of that product. Without the caching, it queries the DB for getting results but after applying caching it goes to the cache store (We use Redis) to get data if the cache is not invalidated, meaning that product’s updated_at property has not been changed (used in the key); thus the data fetch becomes extremely fast.

This is a very simple strategy that can be easily applied nearly everywhere.

4. Performance analysis

All the above solutions will work only if you know where the problem lies but if you do not which usually is the case with bottlenecks. Scout-amp will sniff them out without you having to waste any time and providees a detailed report from n + 1 queries, memory leaks etc. It has an easy, onetime configuration setup.

If you are really keen into getting detailed insight into your application you can nerd yourself out with the new relic. This solution will not only provide the cure but when used regularly, will prevent you from nasty future issues as well.

Sources:

http://www.monitis.com/blog/top-5-ruby-on-rails-performance-tips-for-the-small-business/

https://semaphoreci.com/blog/2017/08/09/faster-rails-eliminating-n-plus-one-queries.html