When a Rails application accepts a request, the controller will usually ask the model for the requested data. The model then fetches it from the database and passes it back to the controller. The controller finally renders the view which represents the data in a human-readable format.
In some situations, rendering the view is an expensive operation, especially if the view needs to display a lot of data, like when showing a list of all available products in a store, for example. In cases like that, caching parts of the returned view can speed things up, especially when the data doesn't change too often.
👋 And if you like this article, there is a lot more we wrote about Ruby (on Rails) performance, check out our Ruby performance monitoring checklist.
Testing caching locally
Caching is turned off in development by default, to make sure you're always getting fresh responses from your app. To test caching locally, you'll have to turn it on in your development configuration.
In Rails 5, you can temporarily turn caching on from the command line. This will use the memory store, meaning the cached fragments will be kept in memory in the web server's Ruby process.
$ rails dev:cache Development mode is now being cached.
You can run the same command to turn caching back off.
Fragment caching
Let's say we have a page that shows all products in a store on a single page. To do that, we have an index view that shows the products.
For each product, the _product.html.erb
partial is rendered, which takes care of displaying a table row with the product's details.
Requesting the page locally, in development mode, with 25 products in the database takes about 300 milliseconds. Almost all of that time is spent rendering the partials.
Most likely, the response will be faster in production, because of bundled assets, less logging and faster web servers. While the numbers in development aren't accurate, they will show which part of our request is slowest, so we can try to speed it up.
Started GET "/products" for ::1 at 2018-03-13 12:16:08 +0100 Processing by ProductsController#index as HTML Rendering products/index.html.erb within layouts/application Product Load (0.4ms) SELECT "products".* FROM "products" Rendered products/_product.html.erb (1.4ms) Rendered products/_product.html.erb (0.4ms) Rendered products/_product.html.erb (0.4ms) Rendered products/_product.html.erb (0.3ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (2.0ms) Rendered products/_product.html.erb (0.9ms) Rendered products/_product.html.erb (0.4ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.7ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.7ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.7ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.9ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.6ms) Rendered products/index.html.erb within layouts/application (257.5ms) Completed 200 OK in 295ms(Views: 290.4ms | ActiveRecord: 0.4ms)
To save time rendering all these partials, we can use Rails' built-in fragment caching, which stores part of the rendered view as a fragment. For subsequent requests, the pre-saved fragment is used instead of rendering it again.
To cache a fragment, you wrap it in a block using the cache
helper.
To see if caching these products sped up our response, we'll request the page twice. The second request should execute a lot faster, as each of the products in the view is pre-rendered and stored in the cache already.
Started GET "/products" for ::1 at 2018-03-13 12:17:29 +0100 Processing by ProductsController#index as HTML Rendering products/index.html.erb within layouts/application Product Load (0.4ms) SELECT "products".* FROM "products" Rendered products/index.html.erb within layouts/application (21.2ms) Completed 200 OK in 55ms (Views: 50.8ms | ActiveRecord: 0.4ms)
It worked! The second request was more than five times faster than the first one. Note that the logs don't show any rendering of the partials, as they're loaded from the cache directly.
Expiring the cache
When calling the cache
helper in the example above, we passed the product
object as the cache dependency. This lets the helper know that the contents of the cached fragment are dependant on the product object.
Internally, the product object has a #cache_key
method, which is used to build a key for the cached fragment. The key for the whole fragment might look like this:
views/products/42-20180302103130041320/75dda06d36880e8b0ae6cac0a44fb56d
The cache key consists of a couple of parts:
- "views/products" is the cache class.
42
is the product's ID20180302103130041320
is the product's updated_at date75dda06d36880e8b0ae6cac0a44fb56d
is a digest of the template tree
This key changes whenever the product is updated, or anything in the template changes. When it does, it causes a miss in the cache, causing the fragment to render again and saving a new fragment. This makes sure the fragment remains fresh, even when its components change.
Beyond fragment caching
While the cached fragments discussed in this example yield a small speedup, there's more to caching than we discussed today. By using strategies like Russian doll caching or lower level methods like caching database query results, bigger speedups can be accomplished.
Of course, we'll go into these in a later episode of AppSignal Academy. Anything specific you'd like to learn about? Please don't hesitate to let us know at @AppSignal. Of course, we’d love to know how you liked this article, or if you have another subject you’d like to know more about.