Summarized using AI

The Ghosts of Action View Cache

Hartley McGuire • July 08, 2025 • Philadelphia, PA • Talk

Introduction

"The Ghosts of Action View Cache," presented by Hartley McGuire at RailsConf 2025, explores the past, present, and future of Rails’ Action View and its underlying caching, template compilation, and code maintenance mechanisms. The talk uses the narrative device of being visited by "ghosts" to walk through real-world problems and innovations in Rails view rendering.

Main Topic

The core theme of this talk is how the foundational components of Action View—especially dependency tracking and render parsing—enable efficient view caching, memory usage optimization, and dead code identification in Rails applications.

Key Points

  • Fragment Caching vs. Low-level Caching:

    • Fragment caching in Action View is introduced as a safer and more efficient alternative to low-level caching with Rails.cache, reducing errors (such as caching relations instead of records) and improving performance by caching rendered HTML.
    • Action View handles cache invalidation automatically using template digests, removing the burden from developers.
  • Cache Invalidation Mechanism:

    • Explains how the ActionView::Digestor generates cache keys based on template content and dependencies, enabling automatic and reliable cache invalidation whenever templates change.
    • The ActionView::DependencyTracker (ERB Tracker) determines which templates are rendered within others, using regular expressions to build dependency trees.
  • Optimizing Memory Usage with Precompilation:

    • Rails' standard template compilation can lead to redundant memory usage across forked worker processes.
    • Introduces the actionview-precompiler gem, which parses and compiles ERB templates at application boot using Ruby’s ripper or prism parser, reducing redundant memory use.
    • The move from regex-based render parsing to real Ruby parsers (Prism in Ruby 3.4) allows for more robust and compatible dependency tracking across Ruby implementations and template languages.
  • Dead Code Identification using Mark-and-Sweep:

    • Dead view templates often accumulate in large or long-lived Rails apps, especially during migrations or UI changes.
    • Describes a CLI tool (inspired by garbage collection mark-and-sweep) that uses Action View’s dependency tracking to find and report unused (dead) templates, allowing for codebase cleanup and further memory savings.
  • Forward-looking Improvements and Recommendations:

    • Advocates for upstreaming precompilation and dead code detection tools directly into Rails for wider accessibility and easier maintenance.
    • Suggests making the Prism render parser the default in Rails 8.1 for more accurate render parsing and integration with various template languages.
    • Emphasizes the utility and extensibility of the foundational Action View components and challenges developers to leverage them for solving future problems.

Examples and Anecdotes

  • Uses a narrative of a solo developer experiencing and fixing production issues to illustrate the practical relevance of caching, memory, and code hygiene problems.
  • Describes personal and team-level experience cleaning up dead templates during a UI library migration, motivating the need for better, automated tools.

Conclusions and Takeaways

  • Fragment caching in Action View offers safer, performance-oriented caching and seamless cache invalidation.
  • Precompiling templates at boot with tools like actionview-precompiler (and leveraging Ruby parsers) significantly reduces memory usage.
  • Automated identification and removal of dead view templates helps maintain cleaner codebases and improves performance.
  • The composability of Action View’s core components presents opportunities for future innovations and should be more tightly integrated and configurable in Rails’ default toolset.

The Ghosts of Action View Cache
Hartley McGuire • Philadelphia, PA • Talk

Date: July 08, 2025
Published: July 23, 2025
Announced: unknown

It's Railsconf Eve and you're trying to sleep, but you keep being awoken by ghosts trying to tell you about Action View! You're taken to the past, where you learn about Russian Doll Caching. You're shown the present, where brand new tools are built on Action View's foundations. And finally you're shown the future, where Rails applications are more powerful than ever.

Through this journey, you’ll gain a deep understanding of Action View and how its primitives can help you solve a wide range of problems. Whether you’re optimizing an existing app or building the future of Rails, you’ll leave knowing how to make your views faster, leaner, and more maintainable.

RailsConf 2025

00:00:17.199 Um, as she said, my name is Harley
00:00:19.039 Magcguire and my talk today is the
00:00:21.039 ghosts of action view cache.
00:00:24.560 But first, a little bit about me. I am
00:00:26.720 part of the Rails issues team. You may
00:00:28.400 have seen my picture on GitHub. I also
00:00:30.400 work at Shopify where for the past few
00:00:32.640 years I've been on our database as a
00:00:34.800 service team which we call Kate SQL
00:00:36.640 which is where we run my SQL on
00:00:38.239 Kubernetes.
00:00:39.840 And in the past few months I've actually
00:00:42.000 had the opportunity to join the Rails
00:00:43.760 infrastructure team which is where I
00:00:45.120 work today. If you have questions about
00:00:47.920 contributing to Rails, our Kate SQL
00:00:50.000 platform or my time so far on the Rails
00:00:52.559 infrastructure team, feel free to come
00:00:54.239 say hi.
00:00:55.920 Now today I won't really be talking
00:00:57.760 about these things. Instead I want to
00:00:59.920 share with you a story
00:01:03.600 to really set the stage since Rails is
00:01:06.159 the oneperson framework. I want you to
00:01:08.000 imagine you're in the shoes of a solo
00:01:09.920 developer. You've been working on a
00:01:11.840 Rails application for a few years now
00:01:13.840 and this year you decide to go to
00:01:15.680 Railscom to really integrate with the
00:01:17.280 community.
00:01:19.200 Now it's the night before Rails comp and
00:01:21.680 you have an idea. I should upgrade my
00:01:24.080 Rails application so I can take
00:01:25.920 advantage of all the cool things I'll
00:01:27.439 hear about at the conference tomorrow.
00:01:30.080 You upgrade your app, CI passes, it
00:01:32.799 deploys to production, and you head to
00:01:35.520 bed.
00:01:38.320 But suddenly, you're startled awake.
00:01:40.320 Your phone is ringing. You're being
00:01:41.840 paged. You pull up your applications
00:01:44.400 dashboards and you see an error. It's
00:01:47.040 coming from a controller, and the code
00:01:48.880 looks like this.
00:01:50.960 And the error points to this cache
00:01:52.479 block.
00:01:55.119 Could the Rails upgrade have broken
00:01:56.560 caching?
00:01:58.240 But then you realize the problem is with
00:01:59.759 your code. This isn't caching a query.
00:02:02.240 It's caching an active record relation.
00:02:04.960 The fix is simple. Make sure that the
00:02:06.719 relation is loaded inside the block so
00:02:09.039 that it caches the queried records.
00:02:12.000 You commit the fix, but as you go to
00:02:14.080 deploy, you feel a strange presence.
00:02:18.000 Who's there? you exclaim, but there's no
00:02:20.640 response.
00:02:22.160 Finally, the silence is broken by the
00:02:24.319 faintest whisper.
00:02:26.720 Use action view.
00:02:30.400 You realize that you've been visited by
00:02:32.080 the ghost of action view past. And it's
00:02:34.640 here to suggest that instead of using
00:02:36.560 low-level caching with Rails.cache, you
00:02:39.120 use action views fragment caching. Let's
00:02:41.760 see what that looks like.
00:02:45.040 Instead of calling Rails cache fetch
00:02:47.200 inside a model or controller, you
00:02:49.440 instead move the expensive or slow
00:02:51.440 queries into your view template and wrap
00:02:53.840 the template render inside a cache
00:02:56.000 block. And this has a few advantages.
00:03:00.319 The first is that you can cache even
00:03:02.000 more of your request. While the slowest
00:03:04.480 part of a request may be a specific
00:03:06.239 query, caching the template enables you
00:03:08.400 to potentially cache even more queries.
00:03:10.959 In additional in uh additionally you can
00:03:13.840 cache the rendering of the final HTML.
00:03:17.120 So action view requ uh caching makes
00:03:19.599 your requests faster.
00:03:22.000 Additionally, you no longer have to
00:03:23.360 worry about what kind of object is being
00:03:25.120 put in the cache. The bug being fixed
00:03:27.680 was caused by a relation being put in
00:03:29.680 the cache instead of a model. But with
00:03:32.000 action view caching, you always cache
00:03:33.680 rendered HTML and you don't have to
00:03:35.920 worry about this kind of bug at all.
00:03:39.680 Now, a question you may have is how does
00:03:41.920 cach invalidation work?
00:03:44.560 If the application's templates are
00:03:46.239 changed and a new version of the app is
00:03:48.400 deployed, how does action view know to
00:03:50.480 render the new template instead of
00:03:52.319 serving HTML from the cache?
00:03:55.519 And this is an area where the magic of
00:03:57.200 Rails really shines. Developers don't
00:03:59.840 need to worry about cache invalidation
00:04:01.680 at all because action view handles it
00:04:03.840 for you transparently. And that is what
00:04:06.239 I really want to talk about today.
00:04:10.319 Cache keys are generated by a class
00:04:12.159 called action view digesttor. Let's see
00:04:14.720 how it works.
00:04:17.280 In this example, we'll have a product
00:04:19.199 show page that renders a product model
00:04:21.840 template and a list of variant model
00:04:24.240 templates. To calculate the cache key
00:04:26.960 for the product show template, we call
00:04:29.199 the digesttor's digest method, which
00:04:31.600 we'll first call the tree method to
00:04:33.759 build a tree representing the templates
00:04:35.520 renders. In this case, the root of the
00:04:38.320 tree is the product show template and
00:04:40.479 its children are the product and variant
00:04:42.560 model templates.
00:04:44.960 Then the digest method uses that tree to
00:04:47.680 generate the cache key. To create the
00:04:50.639 key for show, we first need the cache
00:04:52.400 keys for all of the children. So you can
00:04:54.720 start with the product template. And
00:04:56.400 since it has no children, its cache key
00:04:58.720 will just be a hex digest of the source
00:05:01.280 code of the template,
00:05:03.680 which will look something like this.
00:05:05.919 And then we move on to the next child
00:05:07.440 and digest its source. And now that
00:05:09.759 we've digested all of the children of
00:05:11.600 the show template, we can create the
00:05:13.280 final cache key by hex digesting the
00:05:15.600 combination of the two children digests
00:05:19.039 and the content of the show template.
00:05:21.919 And this is the magic behind cache
00:05:23.840 invalidation. Each template's cache key
00:05:26.479 depends on the content of a template
00:05:28.720 plus all of its dependencies.
00:05:31.360 If any of those templates change, the
00:05:33.199 cache is invalidated and the new version
00:05:35.280 of the template gets rendered.
00:05:37.919 Now, something I kind of glossed over is
00:05:39.759 how the digtor knows which templates are
00:05:42.080 rendered by a template in order to
00:05:44.160 create the tree. This is handled by
00:05:46.479 another class called action view
00:05:48.160 dependency tracker and more specifically
00:05:50.320 ERB tracker. Um, this class constructs
00:05:53.680 this really complicated regular
00:05:55.440 expression which I put here sideways so
00:05:57.440 that you can see how big it is without
00:05:58.880 really trying to read it.
00:06:00.639 um to and it extracts the template names
00:06:02.880 from these render calls um and that
00:06:05.360 tells you the dependency tracker what
00:06:07.280 the dependencies are and naturally
00:06:09.759 because we're using regular expressions
00:06:11.280 this isn't a perfect solution so it also
00:06:13.360 provides a fallback mechanism where you
00:06:15.120 can explicitly write a template's
00:06:16.800 dependencies in a ERB comment
00:06:21.199 and that's how action view does cache
00:06:23.199 invalidation the digtor constructs trees
00:06:26.319 of dependencies and the ERB tracker
00:06:28.639 parses renders
00:06:30.880 Okay, we'll revisit these classes, but
00:06:33.440 for now, let's go back to the story and
00:06:35.360 our solo developer.
00:06:38.400 As a reminder, our developer had to fix
00:06:40.639 a caching bug in their application, and
00:06:43.039 they've now used fragment caching to do
00:06:44.800 so. Wow, they think the conference
00:06:47.120 hasn't even started, and I've already
00:06:48.560 learned something to make my Rails
00:06:49.840 application better.
00:06:51.919 However, at this point, it's way past
00:06:53.759 their bedtime, and they'd really like to
00:06:55.440 get some sleep before the conference
00:06:56.960 kicks off bright and early tomorrow.
00:06:59.759 They push up their fragment caching bug
00:07:01.520 fix. It passes CI, deploys to
00:07:04.160 production, and once again they head to
00:07:06.560 bed.
00:07:08.560 And of course, once again, they get
00:07:10.160 paged. This time when they pull up their
00:07:12.639 dashboards, they see that their
00:07:13.919 application is running out of memory and
00:07:15.680 restarting.
00:07:17.199 Debugging memory issues can be
00:07:18.960 difficult, and it's the middle of the
00:07:20.800 night now, so the solo developer is
00:07:22.639 interested in a quick fix.
00:07:26.000 I have an idea, they think. Rails
00:07:28.560 enables YJIT by default to make
00:07:30.720 applications faster, but it does use
00:07:32.800 more memory. So, I could try turning it
00:07:34.880 off and see if my memory issues go away.
00:07:38.080 But before they can even open up their
00:07:39.840 editor, the feeling of a strange
00:07:42.080 presence returns.
00:07:44.400 And this time, its advice comes quickly.
00:07:47.599 Use action view.
00:07:50.800 The presence is new, but its purpose is
00:07:52.960 the same. to convince you to solve your
00:07:55.039 memory issues with action view. To
00:07:58.160 understand how that's even possible, we
00:08:00.319 first need to understand how Rails
00:08:02.240 servers operate.
00:08:04.960 Most production web servers will launch
00:08:06.720 with some sort of orchestrator process
00:08:08.720 that starts off by eager loading your
00:08:10.800 application and then the orchestrator
00:08:12.720 will fork off a number of workers to
00:08:14.800 actually handle requests.
00:08:16.960 And the major benefit to this eager load
00:08:19.280 then fork pattern is that the workers
00:08:21.599 are able to share a lot of their memory
00:08:23.919 due to an optimization called copy on
00:08:26.000 write which applies as long as the
00:08:28.000 memory isn't changed.
00:08:30.319 To show how important this pattern is
00:08:31.759 for reducing memory usage, let's look at
00:08:33.680 how Rails processes requests.
00:08:37.120 When a request comes into your freshly
00:08:39.200 started application, it gets routed to
00:08:41.279 one of the workers. If that request
00:08:43.519 requires ERB templates to be rendered,
00:08:45.680 it first checks to see if the ERB
00:08:47.680 template for the request has already
00:08:49.200 been compiled to Ruby code. Since the
00:08:51.440 application hasn't processed any
00:08:52.800 requests yet, it compiles the ERB
00:08:54.880 templates and renders a response.
00:08:57.760 Now, what happens if that same request
00:09:00.080 gets routed to a different worker?
00:09:03.279 Since the ERB templates were compiled in
00:09:05.279 the first worker, the second worker
00:09:07.040 doesn't know they exist. So it does its
00:09:09.839 own compilation of the templates and
00:09:11.760 renders a response.
00:09:14.080 And as requests continue to get routed
00:09:16.160 to other workers, they will continue to
00:09:18.320 recompile the same ERB templates and
00:09:21.040 continue to increase your application's
00:09:22.800 memory usage even though the exact same
00:09:25.120 templates are being compiled in each
00:09:27.279 worker.
00:09:28.800 So at this point, the question you are
00:09:30.320 probably asking is why doesn't Rails
00:09:32.399 just compile the templates during eager
00:09:34.320 load so that the memory can be shared?
00:09:37.200 And the answer basically boils down to
00:09:38.800 the fact that it's kind of a hard thing
00:09:40.399 to do.
00:09:42.399 Skipping over a ton of details of how
00:09:44.160 ERB works, the important thing to know
00:09:46.399 is that templates get compiled into
00:09:48.080 regular Ruby methods. And one part of
00:09:51.120 this process is adding a translation
00:09:53.200 layer so to the compiled method so that
00:09:55.440 you can use locals passed into the
00:09:57.040 template just like local variables
00:09:58.959 inside the template itself.
00:10:01.440 But when a template is being compiled
00:10:03.040 into a method, how does it know which of
00:10:05.120 these things are variables and which
00:10:06.959 should be method calls to view helpers?
00:10:09.839 The answer is it can't know without
00:10:11.839 seeing the context, the render method
00:10:14.320 that actually renders the template.
00:10:17.040 So for Rails to be able to pre-ompile
00:10:19.440 all of the ERB templates in your app, it
00:10:22.000 would probably need something that can
00:10:23.519 parse the renders.
00:10:26.800 Wait a second. The ERB tracker already
00:10:28.880 parses renders.
00:10:30.880 Unfortunately, it doesn't quite work in
00:10:32.480 this case because it only grabs the
00:10:33.920 template names out of render calls and
00:10:36.160 we need to know the other arguments, the
00:10:38.000 locals.
00:10:39.519 Maybe the ERB tracker could be improved
00:10:41.519 to parse the locals, but do we really
00:10:43.680 want to tack on more regular expressions
00:10:45.440 to the existing regular expressions?
00:10:48.320 What if instead of using regular
00:10:50.000 expressions to parse all of the Ruby
00:10:51.760 code, we just use Ruby's parser?
00:10:55.760 And this is exactly what John Hawthorne
00:10:57.839 did in a gym called action view
00:10:59.680 pre-ompiler.
00:11:01.519 It uses Ruby's ripper parser to not only
00:11:04.480 extract template names from render
00:11:06.399 methods, but locals as well. And because
00:11:09.519 of this, it's able to compile ERB
00:11:11.360 templates when the application boots,
00:11:13.519 which can lower the total memory usage.
00:11:16.399 The way the gym works is actually quite
00:11:18.240 straightforward. So let's do a quick
00:11:19.760 walk through.
00:11:22.000 The first thing it does is it gathers
00:11:23.760 all of the view templates, view helpers,
00:11:25.920 and controllers in the application. The
00:11:29.040 templates are found using action
00:11:30.560 controller bases view paths, while
00:11:32.640 controllers and helpers are found using
00:11:34.720 the application's paths object.
00:11:38.399 Then it uses the ripper parser I
00:11:40.079 mentioned before to parse the render
00:11:41.760 calls for each of those files. And when
00:11:45.120 parsing these renders, it stores the
00:11:46.560 name of the template being rendered
00:11:48.720 along with the local variables used as
00:11:50.640 well.
00:11:52.560 And once it has gathered all of the
00:11:53.920 template names and locals, the only
00:11:55.760 thing left to do is to actually look up
00:11:58.320 the template class and call its internal
00:12:00.240 compile method. And that's actually all
00:12:02.720 there is to it.
00:12:05.360 As a result of action view pre-ompiler,
00:12:07.519 a ripper implementation of the ERB
00:12:10.000 tracker was actually upstream to Rails
00:12:12.000 as well. And because this new tracker
00:12:14.800 uses a real Ruby parser, it's able to
00:12:17.360 handle many of the edge cases that the
00:12:19.120 ERB tracker just can't. For example, it
00:12:22.160 knows to ignore the string render if it
00:12:24.240 appears in an HTML class. And it also
00:12:26.880 knows to ignore renders in commented out
00:12:29.040 ERB tags.
00:12:31.279 And since we're talking about Ruby
00:12:32.720 parsers, I also have to mention the
00:12:34.639 creation of Prism, which is a brand new
00:12:36.959 Ruby parser, and it became the default
00:12:38.959 in Ruby 3.4.
00:12:41.279 In addition to working on the parser
00:12:42.720 itself, Kevin Newton also contributed
00:12:44.959 prism implementations of the ripper
00:12:46.959 parser to action view pre-ompiler and
00:12:49.200 the dependency tracker in Rails.
00:12:52.160 One shortcoming of the ripper parser is
00:12:54.560 that it can only be used with C Ruby
00:12:56.880 because other Ruby implementations have
00:12:58.880 their own parsers. However, since Prism
00:13:01.680 was built to be integrated with any Ruby
00:13:03.680 implementation, the Prism based render
00:13:05.839 parsers are much more compatible.
00:13:10.240 We've gotten a little bit off track
00:13:11.519 here, so let's take a step back and
00:13:13.440 summarize where we are. Action view
00:13:15.920 pre-ompiler can lower your application's
00:13:18.160 total memory usage by compiling your ERB
00:13:20.880 templates during boot instead of while
00:13:23.200 processing requests.
00:13:25.440 The pre-ompiler works by parsing renders
00:13:27.600 to extract template names and local
00:13:29.680 variables, which it can then use to
00:13:31.440 compile the templates.
00:13:34.320 Now, one more time, let's continue the
00:13:36.480 story of the solo developer.
00:13:40.399 As a reminder, our developer had to fix
00:13:42.480 their application's memory usage, and
00:13:44.480 they've now used action view pre-ompiler
00:13:46.399 to do so. Two pages in one night, they
00:13:49.200 exclaim, "How unlucky. At least now I'll
00:13:52.240 finally be able to get some sleep." They
00:13:55.040 push up their action view pre-ompiler
00:13:56.720 fix. It passes CI, deploys to
00:13:59.199 production, and once again, they head to
00:14:01.680 bed.
00:14:04.079 Now, I'd say the developer gets paged
00:14:06.639 again, but the next thing I'm going to
00:14:08.399 tell you is how action view can be used
00:14:10.240 to find dead code in your application.
00:14:12.399 And who actually gets paged for that
00:14:14.240 kind of thing?
00:14:16.000 Instead, let me tell you my story.
00:14:19.360 Over enough time, I think any
00:14:20.959 application probably ends up with some
00:14:22.399 amount of dead code,
00:14:25.120 especially dead view templates. As files
00:14:28.079 are moved around, new files added, old
00:14:30.320 files removed. Can we really guarantee
00:14:32.079 that we don't leave anything behind?
00:14:35.279 Around a year ago, the application I
00:14:37.279 work on was going through a large
00:14:38.560 migration. We were switching UI
00:14:40.639 libraries.
00:14:42.160 So naturally, this led to a lot of churn
00:14:44.480 in our views. And by the end of the
00:14:46.880 migration, the goal was every file
00:14:48.880 should be rewritten or removed.
00:14:52.240 But I remember there was one week in
00:14:53.839 particular where there was multiple
00:14:55.519 times I noticed we had some dead view
00:14:57.440 templates in our application. and I
00:15:00.240 created pull requests to remove them.
00:15:01.760 But I knew that finding these individual
00:15:03.360 files wasn't really a scalable solution
00:15:05.440 to the problem. What I really wanted was
00:15:07.920 a tool that I could could identify all
00:15:10.240 of the dead templates used in the entire
00:15:12.639 application.
00:15:14.959 I eventually came up with an idea to
00:15:16.720 implement a mark sweep type pattern to
00:15:20.079 identify which view templates must be
00:15:21.920 dead. If you've never heard of MarkX
00:15:24.240 sweep before, it's a algorithm
00:15:26.240 frequently used by garbage collectors,
00:15:28.000 including the default garbage collector
00:15:29.839 in Ruby. And here's what that looks
00:15:32.160 like.
00:15:33.920 We first get a list of all view
00:15:35.760 templates in the application. Then we
00:15:38.240 need to create some list of root
00:15:39.839 templates that we know are used and then
00:15:42.800 we iterate through that list. First
00:15:45.120 marking a template itself as used and
00:15:47.600 then recursing to any templates rendered
00:15:49.759 by that template and marking them as
00:15:51.600 used.
00:15:52.720 And this continues until the end of the
00:15:55.040 root template list at which point every
00:15:57.279 template in the application is now
00:15:59.440 marked as being used or we can sweep
00:16:01.839 away as dead code.
00:16:05.759 I pitched this rough idea to my team and
00:16:08.079 it was enough to nerd snipe one of my
00:16:09.680 teammates into writing a big complex
00:16:12.399 regular expression that would parse the
00:16:14.240 template names out of render calls
00:16:16.000 inside our applications view templates.
00:16:19.360 And at this point, I was like, "Wait a
00:16:21.199 second. I've seen this one before.
00:16:22.880 Action view can already do this with the
00:16:24.720 ERB tracker." With the hard part of the
00:16:27.440 job already implemented in Rails, I
00:16:29.360 started working on a gem.
00:16:32.160 My goal was to create a CLI that you can
00:16:34.480 run in your own Rails application, and
00:16:36.800 it will print out the paths of any view
00:16:38.560 templates that it identifies as dead.
00:16:42.240 As I explained before, the first thing
00:16:43.839 to implement was collecting a list of
00:16:45.759 all the view templates in the
00:16:47.120 application. In the CLI, you can display
00:16:49.600 this list with the all flag. My first
00:16:52.480 approach to creating this list was to
00:16:54.399 use a directory glob so that I could
00:16:56.399 avoid initializing the Rails
00:16:57.839 application. But I quickly learned that
00:16:59.600 this wouldn't work well for finding view
00:17:01.360 templates in gems or even other Rails
00:17:04.079 engines in subfolders.
00:17:06.160 I ended up adding an environment require
00:17:08.720 so that the application is initialized,
00:17:10.640 which is unfortunately much slower than
00:17:12.400 not initializing. However, it's then
00:17:14.559 able to use action controllers view
00:17:16.240 paths, which is much more accurate than
00:17:18.319 the glob that I started with.
00:17:21.919 The next thing to implement was
00:17:23.199 recording each template's dependencies.
00:17:25.600 Template dependencies can be displayed
00:17:27.439 in the CLI along with their templates by
00:17:29.520 passing the trees flag.
00:17:32.320 For this, I created a node class that
00:17:34.320 held a reference to the template name
00:17:36.000 along with all of its children, which I
00:17:38.160 computed using action view dependency
00:17:39.919 tracker. And with this structure in
00:17:42.400 place, it became very easy to
00:17:44.000 recursively list a template's
00:17:45.679 dependencies.
00:17:47.919 If you think that sounds like I
00:17:49.520 reimplemented action view digesttor's
00:17:51.520 tree method, you would be correct. It
00:17:54.240 wasn't until a few months later that I
00:17:55.840 learned about the digtor. But once I
00:17:57.840 did, I was able to dramatically simplify
00:18:00.080 my gyms implementation by replacing all
00:18:02.160 of my custom treeb building code.
00:18:05.039 The last thing to implement is
00:18:06.559 collecting the list of roots which we
00:18:08.720 can identify for sure as not dead. And
00:18:12.160 there are a few different approaches
00:18:13.520 that can be used here. And I'll discuss
00:18:15.120 the trade-offs of each.
00:18:18.080 Throughout the talk, I've been referring
00:18:19.679 to all view templates as templates, but
00:18:22.559 really Rails makes a distinction between
00:18:24.640 templates and partials. The way I think
00:18:27.360 about it generally is that controllers
00:18:29.120 render templates, templates render
00:18:30.880 partials, and partials also render
00:18:32.799 partials.
00:18:34.799 The first and I think simplest strategy
00:18:36.960 is to assume that all templates are used
00:18:39.200 but partials may not be. And as long as
00:18:42.480 your application consistently only
00:18:44.160 renders partials from templates or other
00:18:46.640 partials, this will very accurately
00:18:48.960 identify unused partials in your
00:18:50.799 application.
00:18:52.720 However, it also has a really high
00:18:54.240 chance of false negatives, meaning that
00:18:56.080 partials could be marked as used even if
00:18:58.480 the template that renders them is not
00:19:00.480 used.
00:19:02.640 Another approach that could be used is
00:19:04.640 take inspiration from action view
00:19:06.240 pre-ompiler. Instead of assuming that
00:19:08.720 all templates are used, only mark a
00:19:10.640 template as used if it will be rendered
00:19:12.799 by a controller or helper. And this is a
00:19:15.840 more accurate approach for templates and
00:19:17.600 therefore has a chance to remove many
00:19:19.440 more dead views than if an application
00:19:22.240 has many dead templates with
00:19:23.679 dependencies.
00:19:25.840 The downside is that views marked as
00:19:27.600 dead may not actually be dead. we may
00:19:30.080 have just missed the thing that renders
00:19:31.919 them. Additionally, logic for parsing
00:19:34.720 renders in a controller is not quite the
00:19:37.200 same as the logic for parsing renders in
00:19:39.360 templates. The render parser in action
00:19:42.240 view pre-ompiler is written to handle
00:19:44.320 controller renders, but the dependency
00:19:46.320 tracker in Rails is not.
00:19:48.880 Finally, Turbo can also make this
00:19:50.559 problem a little bit harder because it
00:19:52.160 adds additional methods that can render.
00:19:54.480 So if a template is only rendered by
00:19:56.480 something like a broadcast, then this
00:19:58.080 approach would currently mark it as
00:19:59.520 dead.
00:20:02.160 With either approach for determining the
00:20:04.080 root templates, if you run the CLI with
00:20:06.240 no arguments, you'll get either a list
00:20:08.000 of dead view templates or no output at
00:20:10.720 all if there aren't any.
00:20:13.520 And while I don't think this is ready to
00:20:15.440 be a blocking step in CI, I have had a
00:20:18.400 lot of success running this against my
00:20:20.000 own application. It's correctly
00:20:22.320 identified view templates which are dead
00:20:24.240 that account for almost 5% of the
00:20:26.559 current total of view templates that we
00:20:28.320 have. So that's time saved linting ERB
00:20:31.600 files in CI and it unlocks the potential
00:20:33.840 to remove even more dead Ruby code in
00:20:36.240 the application.
00:20:39.200 Speaking of the future, I guess it's
00:20:40.880 time to address that too. With Action
00:20:43.679 View already solving all of these
00:20:45.280 different problems for us in the
00:20:46.720 present, what more is there for it to do
00:20:49.039 in the future?
00:20:52.320 What I really find so compelling about
00:20:54.880 these three wildly different topics,
00:20:57.520 cache invalidation, pre-ompilation, and
00:21:00.320 dead code identification, is that
00:21:02.400 they're all built on the same
00:21:03.679 foundation. All three of these things
00:21:06.159 depend on render parsing to function.
00:21:08.880 And while action view pre-ompiler
00:21:10.640 already defaults to using the prism
00:21:12.480 render parser when available, action
00:21:14.559 view does not. Meaning the digtor and
00:21:17.440 anything else that builds on it, like
00:21:18.799 loose ERBs, are stuck with a suboptimal
00:21:21.360 implementation.
00:21:23.520 Just this month, someone opened an issue
00:21:25.440 because the ERB tracker attempts and
00:21:27.919 fails to parse HTML attributes that
00:21:30.480 include the string render.
00:21:33.280 Maybe we can improve the regular
00:21:34.960 expressions to fix this edge case. But
00:21:37.600 as long as regular expressions are used,
00:21:39.679 there will probably still be more edge
00:21:41.440 cases to fix. By switching to a real
00:21:44.480 Ruby parser, we eliminate the
00:21:46.159 possibility of edge cases completely.
00:21:49.280 The ripper render parser was added in
00:21:51.120 Rails 7 and the Prism parser in Rails
00:21:53.760 7.2.
00:21:55.280 However, there hasn't been a way for
00:21:56.880 applications to even test out these
00:21:58.960 improvements without using undocumented
00:22:00.960 APIs.
00:22:03.039 Let's fix this in Rails 8.1. The render
00:22:05.840 parser should be configurable and Prism
00:22:08.159 should be the default for new
00:22:09.360 applications.
00:22:12.320 Something else we can improve across
00:22:13.760 these areas is making the integrations
00:22:15.600 tighter. As I mentioned while discussing
00:22:18.159 cache invalidation, Rails magic is just
00:22:20.640 doing the correct thing so that
00:22:22.480 developers don't have to think about
00:22:24.080 certain problems. While I talked about
00:22:26.720 action view pre-ompiler today, I don't
00:22:28.880 really think that you should be adding
00:22:30.240 it to your gym file. You shouldn't even
00:22:32.640 have to know it exists. It should just
00:22:34.799 be a part of Rails.
00:22:37.200 The benefits of upstreaming don't stop
00:22:38.880 at increased performance. Action view
00:22:41.280 pre-ompiler's more advanced render
00:22:42.960 parser would also benefit loose ERBs as
00:22:45.679 it would enable parsing those explicit
00:22:47.520 renders in controllers.
00:22:50.000 Additionally, I'd also love to see loose
00:22:52.080 ERBs upstreamed in some form as well.
00:22:55.760 Action view already provides some
00:22:57.280 similar rig tasks that print the
00:22:59.039 dependencies of a template and the Rails
00:23:01.520 CLI also includes a command for listing
00:23:03.679 unused routes. Lucbs would fit right in
00:23:06.960 with these existing tasks to help
00:23:09.120 developers understand their templates
00:23:10.960 dependencies and keep their view paths
00:23:12.960 nice and tidy.
00:23:14.960 In addition to making loose ERBs better
00:23:17.120 by upstreaming action view pre-ompiler,
00:23:19.520 action view pre-ompiler also benefits
00:23:21.600 from loose ERBs being upstreamed.
00:23:24.400 Template pre-ompilation means that all
00:23:26.559 templates get compiled whereas
00:23:28.400 previously templates would only be
00:23:29.760 compiled on use. Therefore, cleaning up
00:23:32.720 unused view templates in your
00:23:34.240 application means you can have even less
00:23:36.480 memory usage than pre-ompiling templates
00:23:38.880 alone.
00:23:41.440 Finally, I want to reemphasize how cool
00:23:43.520 it is that Rails has these foundational
00:23:45.760 classes that compose to solve such
00:23:48.240 interesting problems. In this talk, I
00:23:50.960 showed how the dependency tracker, which
00:23:52.880 was first added to Rails back in 2012
00:23:55.280 for calculating cache digests, has now
00:23:58.000 been repurposed 10 years later for a
00:24:00.400 very different problem space.
00:24:02.880 The more I reflect on this idea, the
00:24:04.799 more I wonder, what else can we build?
00:24:08.159 What other building blocks does Rails
00:24:10.000 contain that could enable developers to
00:24:12.400 solve some yetto-be identified problem?
00:24:15.360 So, I challenge you the next time you
00:24:17.360 have a problem in your own Rails
00:24:18.880 applications to try building a solution
00:24:21.200 with Rails. Learn more about all the
00:24:24.000 different building blocks it provides.
00:24:26.159 And if you're looking for a great place
00:24:27.760 to start exploring, use Action View.
00:24:30.799 Thank you.
00:24:39.600 I think we have time for questions.
00:24:42.080 Thank you. Sorry. Um, is action view
00:24:44.960 pre-ompiler able to work with other
00:24:47.679 templating languages like slim?
00:24:49.919 That's a great question. It can if it
00:24:52.320 uses the prism render parser or the
00:24:54.640 ripper parser because it's operating on
00:24:57.200 the compiled template like the actual
00:24:59.039 Ruby code. Um, the ERB tracker cannot
00:25:02.720 because it's looking specifically for
00:25:04.480 ERB format things.
00:25:07.200 Um, yeah. Yeah. So if we make if we make
00:25:09.679 prism the default then it will it will
00:25:11.919 work with all templating languages.
00:25:15.360 Any other questions?
00:25:18.720 All right. Thank you for real.
Explore all talks recorded at RailsConf 2025
Ben Sheldon
Sam Poder
Rhiannon Payne
Joe Masilotti
Josh Puetz
Wade Winningham
Irina Nazarova
Tess Griffin
+77