Rails Caching, Pt. 1
Tags: rails, beginners
Published: 2020-07-03 22:57:17

I've been exploring caching recently, mostly at a high level to better understand the importance of a cache and what goes on behind the scenes (check out [Cache Me If You Can](https://dev.to/katkelly/cache-me-if-you-can-ak) and [Implementing an LRU Cache](https://dev.to/katkelly/implementing-an-lru-cache-2eh3) for those posts!). 

It's widely understood that a cache is often the most effective way to boost an application's performance. Web sites that feature a single server and a single database can sustain a load of thousands of concurrent users with caching. 

If you have a Ruby on Rails application, Rails offers a set of caching features out of the box. **This post is going to be part one of a dive into the most common cache techniques and how you can implement caching in development to check it out**!

## Caching in Development
Since caching is only enabled by default in the production environment, **we'll need to enable caching directly in the local development environment.**

But, Rails makes it super easy now (on Rails 5 and above) to set up caching in development. You just need to **run `rails dev:cache` to toggle caching on and off** and if it's turned on, Rails will create a `tmp/caching-dev.txt` file. You also won't need to restart the server, as one would have had to do with older versions of Rails. Here is what it looks like in `config/environments/development.rb`:

```ruby
# Enable/disable caching. By default caching is disabled.
  # Run rails dev:cache to toggle caching.
  if Rails.root.join('tmp', 'caching-dev.txt').exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true

    config.cache_store = :memory_store
    config.public_file_server.headers = {
      'Cache-Control' => "public, max-age=#{2.days.to_i}"
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end
```

## Rails Caching Techniques

**In Rails, there are three different types of caching techniques: page, action, and fragment caching.** 

Rails provides **fragment caching by default.** To use page and action caching, you'll have to add these specific `actionpack` gems to our Gemfile. 

### Page Caching
**Page caching is a cache approach that stores response bodies in files that the web server can access.**
1. A request to endpoint A arrives
2. Its response is determined and gets stored in a file B
3. Next time A is requested, the web server sends B directly

It only applies to `GET` or `HEAD` requests with response code 200. This can result in a quick response because the **cached endpoints are short-circuited by the web server and don't even reach your Rails application.** However, this approach only works for pages that don't need to go through the entire Rails stack. Anything that involves authentication and account-based systems are not good contenders.

As of Rails 4, page caching has been removed, but you can access and use it by adding `actionpack-page_caching` to your `Gemfile`.

### Action Caching
Action caching is similar to page caching except the incoming web request hits the Rails stack through Action Pack so that any authentication and before filters can be run before the data in the cache is served. 

As of Rails 4, action caching has been removed, but you can access and use it by adding `actionpack-action_caching` to your `Gemfile`.

## Fragment Caching
By default, Rails offers fragment caching. Depending on the needs of your web application, you can utilize fragment caching to cache different parts of the page. This allows flexibility in what parts you want cached and when it should expire. Fragment caching lets a fragment of view logic (partials, widgets, etc.) to be wrapped in a cache block and served out of the cache store when the next request is made. 

As an example, if you have a travel app and wanted to cache each destination on a page, the code could like something like this:
```ruby
<% @destinations.each do |destination| %>
  <% cache destination do %>
    <% render destination %>
  <% end %>
<% end %>
```
Rails will write a new cache entry with a unique key when your app gets its first request to this page. The key will resemble the following:
```ruby
app/views/destinations/
1-202006026193031061005000/bea67108094918eeba42cd4a6e786901
```
The number after destinations is the `product_id` followed by a timestamp value of the `updated_at` attribute of the destination record. This timestamp ensures that Rails is not serving stale data. If `updated_at` has changed, a new key will be generated and Rails will write a new cache to that key. The old key won't be used again. This is key-based expiration in a nutshell.

If the view fragment itself changes (if the HTML changes) the cache fragment will also be expired.

You can also conditionally cache a fragment using `cache_if` or `cache_unless`:
```ruby
<% cache_if beachside?, destination do %>
  <%= render destination %>
<% end %>
```

### Collection Caching
The `render` method also allows you to cache individual templates rendered for a collection. The below example can read all cache templates at once instead of one by one like above using `each`.

```ruby
<% render partial: 'destinations/destination', 
collection: @destinations, cached: true %>
```

### Tested
When I played around with implementing the cache, my initial page load after all of the database queries was 316ms:
```bash
Write fragment views/destinations/show:
ea9a60feb104fccbb55d6feb00a7dcb8/hacks/265-20200528222447858679 (0.1ms)

Rendered destinations/show.html.erb within layouts/
application (Duration: 187.9ms | Allocations: 26579)

Completed 200 OK in 316ms (Views: 141.6ms | ActiveRecord:
120.6ms | Allocations: 65663)
```
When I reloaded the page, the time was cut down to 62ms and the fragments were taken from the cache!
```bash
...
Read fragment views/destinations/show:ea9a60feb104fccbb55d6feb00a7dcb8
/hacks/265-20200528222447858679 (0.2ms)

Rendered destinations/show.html.erb within layouts/
application (Duration: 8.2ms | Allocations: 1860)

Completed 200 OK in 62ms (Views: 56.9ms | 
ActiveRecord: 2.0ms | Allocations: 29929)
```

Next week, I'm going to look into Rails Cache Stores as well as other caching techniques I didn't cover here. (*Edit: [Rails Caching, Pt. 2](https://dev.to/katkelly/rails-caching-pt-2-2c68) is up!)* 

Happy caching!

*Resources*
[Caching With Rails: An Overview - Rails Guides](https://guides.rubyonrails.org/caching_with_rails.html)
[Caching Strategies for Rails - Heroku](https://devcenter.heroku.com/articles/caching-strategies)