loading...

Collection Caching in Rails with Local Variables

briankephart profile image Brian Kephart ・2 min read

Yesterday I was trying to improve the performance of collection rendering in my rails app. The page I was working on is a schedule, which renders many collections of various data types.

The basic method for rendering a collection in Rails using a specific view file is this:

<%= render partial: 'my_template', collection: 'my_collection' %>

For basic cases, you can add caching like this:

<%= render partial: 'my_template', collection: 'my_collection', cached: true %>

My problem is that different user have different permissions to act on items on the schedule, so the rendering is specific to the user. This requires caching the current_user as a local variable.

I could not find this anywhere in the Rails guides or docs, but I did find the option in the code. The syntax is this:

<%= render partial: 'my_template',
           collection: 'my_collection',
           cached: ->(collection_item) { [collection_item, current_user] } %>

If that syntax is unfamiliar, it's a lambda literal. It will be called for each item in the collection, using that item as an argument. I'm using the lambda to return an array that will be used to make the cache key. This works beautifully for my purposes.

Now, normally with Rails, any methods or syntax not in the guides or API docs are subject to change without deprecation, putting you on thin ice. You're at risk of relying on code that the framework maintainers consider an implementation detail, not public API. In this case, though, the git blame led me to this pull request in which it seems pretty clear that this feature was added for public use.

I'm posting this as a discussion to see if anyone else is using this feature, or knows its history, or knows a reason not to use it.

Posted on Jun 26 by:

briankephart profile

Brian Kephart

@briankephart

I play guitar and bass. Sometimes I code.

Discussion

markdown guide
 

If you can isolate what it is about the user that causes the differences, you should use that as part of your cache key instead of current_user.

e.g. If you look at 'current_user.admin?' to decide what to display, then that part of the code becomes:

cached: ->(collection_item) { [collection_item, current_user.admin?] }

And now you have only 2 versions of the cached item instead of one for each user. Getting this right is tricky, but worth it!

One thing I hate about Rails Views is that @ variables (and helper methods like current_user) can be used anywhere, and someone can come along at any time, add an @testing_mode or whatever to your partial and break the site until Customer Support gets calls about 'weird stuff happening'

 

That would be ideal. In this case however, we have a variety of roles (owner, manager, assistant manager, teacher, substitute teacher), all with different permissions. Isolating those roles would still be an improvement, except there is still behavior specific to whether a user is attached to the item in another, non-role-based way (like this specific person is the teacher of a class, so they can check it out). So the user goes in the cache key.

Still, employees interact with the schedule repeatedly throughout the day, and items on the schedule can be shared between daily/weekly/individual/all-employee views, so even at the user level caching pays off.

Any thoughts on the lambda use? I haven't gotten any comments yet (here or on Twitter) indicating that anyone else has used it. I also can't tell whether the usage is really, really officially supported (Good: specifically approved via PR! Bad: Not documented anywhere outside of the PR).

 

I've never seen this lambda version of the cache key. The Rails collection cache code is quite easy to read. This is the line from github that gets your lambda - and if you don't provide one, they create a default one!

github.com/rails/rails/blob/b378bd...

I usually create a wrapper object that implements a cache_key method to do the key generation. It also is then used inside the partial, so it's a little more noticable when you use data that isn't part of the cache key

For your case, you can do more in here for a complicated case

[ collection_item, current_user.user_role, current_user.teaches_class?(collection_item) ]

I've had object IDs and 4-5 booleans or enums in my cache key before now. Having 30 possible variants is still better than having 1 per user (unless you have only 30 users, in which case you may have optimized too soon)

 

I think View Components is going to improve things a lot in this area by isolating the 'partial' from the global context to reduce this backdoor data access that breaks caching

 

I haven't used that library, but yeah, isolated context with all variables passed in as arguments would make it harder to screw up the caching.