Skip to content

Expire memoized template digests when a component registers - #2709

Open
erikaxel wants to merge 1 commit into
ViewComponent:mainfrom
erikaxel:vc-digest-registration-invalidates-cache
Open

Expire memoized template digests when a component registers#2709
erikaxel wants to merge 1 commit into
ViewComponent:mainfrom
erikaxel:vc-digest-registration-invalidates-cache

Conversation

@erikaxel

@erikaxel erikaxel commented Sep 2, 2026

Copy link
Copy Markdown

Problem

ViewComponent::CacheDigest.enabled? is !registry.empty?, and dependencies_in returns [] while it's false. The registry only fills as components are autoloaded. Under lazy loading — development, and any test environment without eager_load — a digest computed before the first component loads silently omits every component dependency, and ActionView::Digestor memoizes it in DetailsKey.digest_cache, so the wrong value sticks for the life of the process.

The digest is therefore not a pure function of the source: same files, same code, different answer depending on load order.

Reproduction

An app/views partial with a cache block around render SomeComponent.new, where SomeComponent includes ExperimentallyCacheable:

finder = -> { ActionView::LookupContext.new(ActionController::Base.view_paths) }
digest = ->(p) { ActionView::Digestor.digest(name: p, format: :html, finder: finder.call) }

digest.call("vc_cache_probe/_host")
# => "5d2c78aeba543f6517a00d9f27a4d790"   registry empty

SomeComponent                                 # referencing it registers the component
ActionView::LookupContext::DetailsKey.clear
digest.call("vc_cache_probe/_host")
# => "0190488ed202a6ee3b0a6227a1b6f9cc"   registry size 1

No files changed between those two calls.

Impact

Production is mostly safe, because eager_load = true loads every component at boot. Development and test are not: the same fragment cache block can key differently between two boots, and a fragment digested early in a process silently loses its component dependencies. It also makes the feature confusing to evaluate — this is what made our first attempt to verify the advertised behaviour look like the feature simply did not work.

The sandbox suite doesn't catch it because test/sandbox/config/environments/test.rb sets config.eager_load = true.

Fix

CacheDigest.register now expires Action View's memoized digests when it adds a new entry, so a digest computed before a component registered is recomputed rather than served from memory. Re-registering an unchanged component returns early, so reloads and repeated registrations don't churn.

The reset is deliberately narrow: DetailsKey.digest_caches.each(&:clear) drops memoized digests only and leaves resolver caches alone, since no template changed — only the set of dependencies the Digestor can see. DetailsKey.digest_caches is public API in every supported Action View (7.1 through main). DetailsKey.clear also works but throws away resolver caches for no reason.

Registration only happens on class load, so the churn is bounded and stops once everything has loaded.

Alternative considered

Eagerly registering every component at boot instead. That is harder to do correctly under lazy loading, since it means discovering components independently of the autoloader, which is why invalidating on register looks like the better trade.

Tests

  • test_digest_does_not_depend_on_when_the_component_registered (integration) — a fragment digest is identical whether the component registered before or after the template was first digested. Fails on main.
  • test_registering_an_unchanged_component_leaves_memoized_digests_alone and test_registering_a_new_component_expires_memoized_digests (unit) — cover both branches of the new guard.

bundle exec rake passes on Rails 7.1, 7.2, 8.0 and 8.1. The only failures seen were pre-existing and unrelated: allocation-count assertions in RenderingAllocationsTest on 8.0, and the Ruby/Rails version-matrix fixture on 7.1 when run under a Ruby other than the pinned one.

- Return early from CacheDigest.register when the entry is unchanged, so
  reloads and re-registrations don't churn
- Clear ActionView's digest caches on a new registration, leaving resolver
  caches alone
- Add a regression test asserting a fragment digest is the same whether the
  component registered before or after the template was digested
@erikaxel

erikaxel commented Sep 2, 2026

Copy link
Copy Markdown
Author

Hi, as probably can be deduced from the PR description itself, this was created with the help of Claude.

The background is that we are testing the new experimental cache support landed in #2685 and released in 4.15.0.
We are running around 1000 view components, and use them all they way, from layout and out.
This is the first of a couple of fixes that we will suggest based on implementing and testing cache in our project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant