Application performance has probably never been as important as it is now. It is easy to lose potential customers if your application isn’t at least as fast as your competitors’. And if you want to improve performance, you can typically do so most effectively by making some changes to the application itself, rather than by simply adding more nodes. Sure, adding more may increase your app’s performance, but it comes at the cost of more resources. By improving the application code, you get better performance without an increase in hosting costs.
In this article, I explain how to make changes to a Ruby app to improve performance.
Optimizations not language-specific
Let’s start by discussing one of the easiest places to make performance optimizations: the UI.
If you are building an application with a user interface, there are a couple of areas that will make a big difference in the client experience, without adding nodes or changing code.
First up is optimizing image sizes. Why include a 1024×768 image for a 300×200 pixel space (or when loaded on a mobile device, 180×120)? Create two images and have the app dynamically load the correct one. Many consumer web platforms like SquareSpace.com will create seven or more different copies of an image with varying sizes so the smallest image possible can be used, depending on the device.
<!-- Add the plugin by putting something like this between the head tags --> <script src='https://raw.githubusercontent.com/kvendrik/responsive-images.js/master/responsive-img.min.js'></script> <!-- This is the sample img tag using the framework. It has three image sizes --> <!-- based on the screen width. Below 480px, below 960px, and above 960px. --> <img alt='UpCloud logo' data-src=' <480:logo-small.jpg, <960px:logo-medium.jpg, >960px:logo-large.jpg ’>
Finally, you can add compression to any web (HTTP) servers you are using. Whether you are using NGINX or Apache HTTPD, there is a compression module available that will reduce load times for clients and improve their experience. A simple web search will lead to a page like “NGINX Compression and Decompression,” which gives all the details on how to set it up.
Reduce data calls
When it comes to ways to improve performance, let’s start with the idea of reduction. The easiest thing to reduce that will provide the highest value is excess queries to the database. Reducing the number of database calls, especially in Ruby on Rails apps, will involve restructuring the way the application works with data. Actively retrieving more data per call instead of lazy loading will allow the application to parse data in memory, which is much faster than the average database, especially as the application is scaled out and the overall number of active database connections will become an issue. Note that it isn’t advisable to pull ALL the data every time, but you can find a balance that reduces calls and will still use a manageable amount of memory.
An example situation would be around order history. When a client asks for a record of their orders in the last three months, and you know that the vast majority of clients place two orders per month, then write the query to pull back 10 records at a time. This way, you will immediately eliminate up to nine database calls per client per request. This also helps if, for example, you only show five records on the UI and you already have the next five records in memory, which can be displayed while you go back to the database for any additional records without the user seeing any delay.
If you are using active_record_querying, then making this change can be as simple as changing a line of code from:
order = Order.find(1)
order = Order.find(1,2,3,4,5,6,7,8,9,10)
Which will return an object with multiple records in it.
Cache everything you can fit in the memory available to your application. Reuse the data that your application queried from the database. Cache static content so it doesn’t need to be read from disk every time. And if you are building a stateful application, build a cache indexed by that state so each subsequent request by the client can access processed and organized data (which can drastically improve response time). One of the most common tools used to support caching in Ruby is memcached and it’s most widely used Ruby Gem to access it is Dalli.
Once Dalli is added to the gemfile and Ruby is configured to use it then it is a matter of picking how you want to cache data. Rails has several options including page_caching for pages with no filters, action_caching for use with pages that have filters like authentication, fragment caching for partial pages or widgets, and low level caching which uses Rails.cache directly.
Below is sample code to access a cache that contains all out-of-stock items. If the cache is empty or older than five minutes, the fetch will execute the code block, return the results and repopulated the cache.
def Product.out_of_stock Rails.cache.fetch("out_of_stock_products", :expires_in => 5.minutes) do Product.all.joins(:inventory).conditions.where("inventory.quantity = 0") end end
In line with reducing the number of database connections, it is ideal to recycle any and all open TCP connections to and from the application. Two big areas for this include using HTTP 1.1 and 2.0, and using connection pooling on any database connections. While the milliseconds it takes to tear down and build a new TCP connection seem minor, on a busy application with hundreds of requests a minute, this can start to become a noticeable drag on performance, since TCP handling is prioritized above non-kernel activity like Ruby code. There are multiple tools to handle database pooling from open source frameworks for pure Ruby, like ezpool and ActiveRecords Adapters with Ruby on Rails.
Performance is a tricky thing. It is amazing how much wasted processing time can accumulate—and a few minor tweaks can make a world of difference.