loading...

Cache large drop-downs in Rails

epigene profile image Augusts Bautra ・2 min read

Today I noticed that a couple of drop-downs in a key form were taking several seconds to load.

At first I figured it's a slow query, because the drop-downs load data from the DB and there's several dozen records. But, no, queries take only a couple of ms. It's the rendering that is slow.

Implementing a view cache for the drop-downs is straightforward, just have to come up with an expirable key.

The two core elements that can expire the cache are dropdown elements changing, and the selected value changing.
Usually with view caches it is also prudent to scope the key to the resource id or other unique piece of data.

In my case dropdown holds all the Things in the DB. A handy way to check for changes is to look up the most recent updated_at and expire if that's changed. This approach works well for new records and updates,
but will not expire the key if a record gets deleted. Usually this is not a big problem though, especially if the records are managed infrequently. If, however, it is important that deleting a record expires the cache,
adding the count of records may be necessary (and computationally expensive!)

Hacking away I realised that the scoping to resource id is not needed at all,
because I only care about the selected value.
So I got an idea to cache the dropdown globally and do some after-processing with gsub, rather than calculating keys and storing separate cache entries.

The after processing is simple, but detail-specific.
In this case if I cache the dropdown HTML globally, I needed to make sure to remove the 'selected' attribute from whatever option (if any!) has it in the cached HTML, and add it to the appropriate option.

Here's my final solution:

:ruby
  key = cache_key(
    locale: I18n.locale,
    in: "app/views/users/_edit.field.thing_id.html.haml",
    for: "thing_dropdown",
    # lookin up the most recent update time, will expire on another update
    most_recent_thing_change:
      Thing.pluck("MAX(#{ Thing.table_name }.updated_at) AS stamp").last.to_s
  )  

  thing_dropdown =
    Rails.cache.fetch(key, expires_in: 24.hours) do
      select_options = options_from_collection_for_select(
        Thing.all.order(:name), "id", "to_text", user.thing
      )

      render(
        "edit.field_type_item",        
        select_options: select_options,
        include_blank: true,
        input_attributes: {multiple: false}
      )
    end

  # clear whatever is selected in the global cache
  thing_dropdown.gsub!(%r'\s*selected=\S+\s*', " ")
  # make the correct option selected
  thing_dropdown.gsub!(%r'value="#{ user.thing }"', "value=\"#{ user.thing }\" selected=\"selected\"")

= thing_dropdown.html_safe

Posted on by:

epigene profile

Augusts Bautra

@epigene

Senior Rails developer from Latvia. Avid gamer. Longevity enthusiast. #keto-dude

Discussion

markdown guide