Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
e2af193
add component controlled cache
Oct 8, 2024
d71dc5f
add changelog
Oct 8, 2024
e7f7397
fix cacahe implementatation to work with all methods
Oct 15, 2024
9a21b4c
fix cacahe implementatation to work with all methods
Oct 15, 2024
6d2462e
fix lint
Oct 15, 2024
a8073b7
yeah I know it aint working, I am tired however, taking a nother look…
Oct 16, 2024
7163934
fix cache
Nov 5, 2024
fe41b05
fix lint
Nov 5, 2024
a415840
fix
Nov 5, 2024
f8215a2
modulerize code
Nov 5, 2024
05091d5
more cleanup
Nov 5, 2024
3d22c2b
Apply suggestions from code review
reeganviljoen Nov 7, 2024
f1773bd
Update lib/view_component/cacheable.rb
reeganviljoen Nov 7, 2024
ccc755a
fix alphebtization
reeganviljoen Nov 18, 2024
c9622eb
add cache suhggestions
reeganviljoen Nov 19, 2024
d142634
fix legacy ruby specs
reeganviljoen Nov 21, 2024
10ffb42
Apply suggestions from code review
reeganviljoen Mar 23, 2025
2c87f77
code review feedback
reeganviljoen Mar 26, 2025
2aa0b30
make module fully optional;
reeganviljoen Mar 26, 2025
d6a2516
fix specs
reeganviljoen Mar 26, 2025
6094406
fix lint
reeganviljoen Mar 26, 2025
e5de30e
fix coberage
reeganviljoen Mar 26, 2025
8e971d5
add inherited component test
reeganviljoen Mar 26, 2025
f5c2fce
fix tests
reeganviljoen Mar 26, 2025
e3425a5
merge inherited values
reeganviljoen Mar 26, 2025
bc894d6
fix tests
reeganviljoen Mar 26, 2025
b79c3eb
fix lint
reeganviljoen Mar 26, 2025
60cf752
add polish
reeganviljoen Mar 27, 2025
48a222f
add wip docs
reeganviljoen Mar 27, 2025
bf9ea17
fix tests
reeganviljoen Mar 27, 2025
f6f19b7
fix lint
reeganviljoen Mar 27, 2025
87359f9
fix coverage
reeganviljoen Mar 27, 2025
4dcd622
fix lint
reeganviljoen Mar 27, 2025
17389e3
fix lint
reeganviljoen Mar 27, 2025
a6f7710
fix missing coverage
reeganviljoen Apr 1, 2025
13a4417
add format and varaiant to cache_digest
reeganviljoen Apr 1, 2025
d912555
add format and varaiant to cache_digest
reeganviljoen Apr 1, 2025
7413ad5
fix coverage
reeganviljoen Apr 1, 2025
7ed8d28
add retrive ccache key to be consistent with rails
reeganviljoen May 1, 2025
b2807a7
Fix linting
reeganviljoen May 1, 2025
a1d8421
fix changelog stuff
reeganviljoen May 1, 2025
d03928c
refactor cache logic
reeganviljoen May 2, 2025
64636b4
add identifier
reeganviljoen May 2, 2025
7876b14
compuye cache keys
reeganviljoen May 5, 2025
8a21be1
fix lint
reeganviljoen May 5, 2025
540b2d8
refactor
reeganviljoen May 6, 2025
1b2988a
add set
reeganviljoen May 6, 2025
cf313a9
Add cache refistry and alighn cache with how action view does it
reeganviljoen May 6, 2025
03683fe
namespace registry
reeganviljoen May 6, 2025
a0c74eb
add magic comment
reeganviljoen May 6, 2025
8f45ac8
fix failing rails 6 specs
reeganviljoen May 7, 2025
50943a1
Add the start of an actual digestor
reeganviljoen May 7, 2025
da7c685
add template digetor that usses an ast
reeganviljoen Jul 2, 2025
40b5040
refactor digetor a bit
reeganviljoen Jul 2, 2025
d9bb9f1
fix indentation
reeganviljoen Jul 2, 2025
52607fc
refactor
reeganviljoen Jul 3, 2025
c771954
get pr up top date
reeganviljoen Jul 3, 2025
5af2bc7
fix merge issue
reeganviljoen Jul 3, 2025
9d88e3a
try get tests to pass
reeganviljoen Jul 3, 2025
3542beb
try get tests to pass
reeganviljoen Jul 3, 2025
4f58de4
fix rails 8 test
reeganviljoen Jul 3, 2025
b75e0c2
fix soem artifcats
reeganviljoen Jul 3, 2025
1d81e9d
fix test
reeganviljoen Jul 3, 2025
35f68a8
make primer pass
reeganviljoen Jul 3, 2025
1102a50
fix linting
reeganviljoen Jul 3, 2025
b5a6587
fix linting
reeganviljoen Jul 3, 2025
299ff9d
fix alloactor spec
reeganviljoen Jul 3, 2025
9aa9dd0
fix tests
reeganviljoen Jul 3, 2025
f2df735
make primer pass
reeganviljoen Jul 3, 2025
9e93a5a
add inline erb cache component test
reeganviljoen Jul 3, 2025
aa4ff0d
Merge branch 'main' into rv_add_component_caching
joelhawksley Jul 14, 2025
189f212
Merge branch 'main' into rv_add_component_caching
Jan 28, 2026
250084b
fix ci
Jan 28, 2026
703ff01
fix primer ci failures by making ast lazy load dependecies
Jan 28, 2026
430616c
treat `temple`, `slim`, `haml` as optional dependencies (no boot-time…
Jan 28, 2026
0a0d319
fix ci
Jan 28, 2026
9e3767f
Refine fragment caching key + invalidation
Jan 28, 2026
3cc370b
Normalize cache_on dependencies and memoize digests
Jan 29, 2026
fdb9b40
update docs
Jan 29, 2026
63fd953
refactor dependecy extractor
Jan 29, 2026
a1b432e
Merge branch 'main' into rv_add_component_caching
reeganviljoen Jan 29, 2026
a91cc6e
Merge branch 'main' into rv_add_component_caching
joelhawksley Feb 4, 2026
9e4bc2a
Optimize digest and template dependency extraction
Jan 29, 2026
dfc6fe6
code review easy wins
Feb 12, 2026
2967a5e
code review: added a dedicated regression test for child partial depe…
Feb 12, 2026
4822177
code review: added shared integration-style spec approach from fork f…
Feb 12, 2026
805392d
code review: consider reusing parser approach from existing project i…
Feb 12, 2026
ac584e4
add cacing benchmark
Feb 12, 2026
2471962
Update docs/guide/caching.md
reeganviljoen Feb 12, 2026
3d4131a
fix code that was moved incorectly
Feb 12, 2026
a095f98
imrpove performnce
Feb 12, 2026
42d7f7b
add refactors to improve benchmarks
Feb 12, 2026
569fd91
make benchmark cmponents more expensive too really show ccache perfor…
Feb 12, 2026
2aaf6d8
Merge branch 'main' into rv_add_component_caching
Feb 12, 2026
797c0d5
fix ci
Feb 12, 2026
81648c1
fix ci
Feb 12, 2026
e70101e
add test for joels noted limitation
Feb 12, 2026
ca61171
fix tests
Feb 12, 2026
129765e
fix ci
Feb 12, 2026
98e2af9
use ActionviewPrecompiler instread of our own stuff
Feb 12, 2026
8c690b9
add action view precompiler
Feb 13, 2026
304c9f4
Merge branch 'main' into rv_add_component_caching
Feb 13, 2026
5d868d7
fix tests
Feb 13, 2026
c1afc0e
fix accidental commits
Feb 13, 2026
0208862
fix rails-main
Feb 13, 2026
bfa254b
fix rails main
Feb 13, 2026
cbb7d02
fix ci
Feb 13, 2026
78b526a
fix ci
Feb 13, 2026
d081520
fix rails main
Feb 13, 2026
074d380
fix vale issues
Feb 13, 2026
4509dd8
Address component caching review feedback
Jun 14, 2026
0fafb49
Merge branch 'main' of https://github.com/ViewComponent/view_componen…
Jun 14, 2026
ee97af1
Fix CI failures after merge
Jun 14, 2026
53d7487
Refactor lint-sensitive chains
Jun 14, 2026
94ee67c
Handle Ruby head ERB line offsets
Jun 14, 2026
7d1ca48
Keep CI fix scoped
Jun 14, 2026
df9b131
Keep component caching isolated
Jun 14, 2026
2b31019
Stabilize rendering allocation check
Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ group :development, :test do
gem "simplecov", "< 1"
gem "slim", "~> 5"
gem "sprockets-rails", "~> 3"
gem "standard", "~> 1"
gem "standard", "~> 1.54.0"
gem "tailwindcss-rails", "~> 4"
gem "turbo-rails"
gem "warning"
Expand Down
6 changes: 5 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
view_component (4.12.0)
actionview (>= 7.1.0)
actionview_precompiler (>= 0.4)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)

Expand Down Expand Up @@ -55,6 +56,8 @@ GEM
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actionview_precompiler (0.4.0)
actionview (>= 6.0.a)
activejob (8.1.3)
activesupport (= 8.1.3)
globalid (>= 0.3.6)
Expand Down Expand Up @@ -456,7 +459,7 @@ DEPENDENCIES
simplecov-console (< 1)
slim (~> 5)
sprockets-rails (~> 3)
standard (~> 1)
standard (~> 1.54.0)
tailwindcss-rails (~> 4)
turbo-rails
view_component!
Expand All @@ -473,6 +476,7 @@ CHECKSUMS
actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3
actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2
actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d
actionview_precompiler (0.4.0) sha256=33b6bd6ec4c1b856e02fdf5f6512c9eb4a92ac1c0545e941b3e354b7d540ed1c
activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3
activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219
activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ nav_order: 6

## main

* Add experimental support for caching by including `ViewComponent::ExperimentallyCacheable`.

*Reegan Viljoen*

## 4.12.0

* Fix stale render context on reused component instances. A `ViewComponent::Base` instance memoized its controller, helpers, request, view context, lookup context, view flow, and requested format details on first render via `||=`. Rendering the same instance a second time (intentionally or via aliasing) reused that stale context, which could leak data across requests, sessions, or users. `#render_in` now resets these ivars on every call so each render derives its context from the current view.
Expand Down
95 changes: 95 additions & 0 deletions docs/guide/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
layout: default
title: Caching
parent: How-to guide
---

# Caching

Experimental
{: .label }

Caching is experimental. To cache a component, include `ViewComponent::ExperimentallyCacheable` and declare cache dependencies using `cache`:

```ruby
class CacheComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable

attr_reader :foo, :bar

cache do
[foo, bar]
end

def initialize(foo:, bar:)
@foo = foo
@bar = bar
end
end
```

```erb
Comment thread
joelhawksley marked this conversation as resolved.
<p><%= Time.zone.now %></p>
<p><%= "#{foo} #{bar}" %></p>
```

Components that include `ViewComponent::ExperimentallyCacheable` but do not call `cache` render normally without fragment caching.

Check failure on line 36 in docs/guide/caching.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Microsoft.Contractions] Use 'don't' instead of 'do not'. Raw Output: {"message": "[Microsoft.Contractions] Use 'don't' instead of 'do not'.", "location": {"path": "docs/guide/caching.md", "range": {"start": {"line": 36, "column": 70}}}, "severity": "ERROR"}

Caching only reads and writes fragments when controller caching is enabled.

## Dependencies

The `cache` block is evaluated in the component instance context. Returned values are expanded via `ActiveSupport::Cache.expand_cache_key`, so Active Record models, `GlobalID`, arrays, plain strings, and values returned by private methods work as expected.

```ruby
class UserComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable

def initialize(user:)
@user = user
end

cache do
[@user]
end
end
```

## Conditional caching

Use `cache_if` to cache only when a condition is met:

```ruby
class UserComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable

cache_if :cacheable?

cache do
[@user]
end

def initialize(user:, cacheable: true)
@user = user
@cacheable = cacheable
end

private

def cacheable?
@cacheable
end
end
```

`cache_if` accepts a symbol, a boolean value, or a block.

## Cache invalidation

The cache key includes a digest of the component, its sidecar files, and ViewComponents rendered by the component.

Caches are invalidated when the component source, sidecar templates, sidecar translations, or rendered child ViewComponents change.

## Limitations

Changes to partial and layout string dependencies will not invalidate the cache. Modify `RAILS_CACHE_ID` or `RAILS_APP_VERSION` to invalidate these caches on deploy.

Check failure on line 95 in docs/guide/caching.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Microsoft.Contractions] Use 'won't' instead of 'will not'. Raw Output: {"message": "[Microsoft.Contractions] Use 'won't' instead of 'will not'.", "location": {"path": "docs/guide/caching.md", "range": {"start": {"line": 95, "column": 51}}}, "severity": "ERROR"}
3 changes: 3 additions & 0 deletions gemfiles/rails_7.1.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
view_component (4.12.0)
actionview (>= 7.1.0)
actionview_precompiler (>= 0.4)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)

Expand Down Expand Up @@ -60,6 +61,8 @@ GEM
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actionview_precompiler (0.4.0)
actionview (>= 6.0.a)
activejob (7.1.6)
activesupport (= 7.1.6)
globalid (>= 0.3.6)
Expand Down
3 changes: 3 additions & 0 deletions gemfiles/rails_7.2.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
view_component (4.12.0)
actionview (>= 7.1.0)
actionview_precompiler (>= 0.4)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)

Expand Down Expand Up @@ -55,6 +56,8 @@ GEM
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actionview_precompiler (0.4.0)
actionview (>= 6.0.a)
activejob (7.2.3)
activesupport (= 7.2.3)
globalid (>= 0.3.6)
Expand Down
3 changes: 3 additions & 0 deletions gemfiles/rails_8.0.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
view_component (4.12.0)
actionview (>= 7.1.0)
actionview_precompiler (>= 0.4)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)

Expand Down Expand Up @@ -52,6 +53,8 @@ GEM
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actionview_precompiler (0.4.0)
actionview (>= 6.0.a)
activejob (8.0.4)
activesupport (= 8.0.4)
globalid (>= 0.3.6)
Expand Down
3 changes: 3 additions & 0 deletions gemfiles/rails_8.1.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
view_component (4.12.0)
actionview (>= 7.1.0)
actionview_precompiler (>= 0.4)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)

Expand Down Expand Up @@ -55,6 +56,8 @@ GEM
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actionview_precompiler (0.4.0)
actionview (>= 6.0.a)
activejob (8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.3.6)
Expand Down
4 changes: 4 additions & 0 deletions gemfiles/rails_main.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ PATH
specs:
view_component (4.12.0)
actionview (>= 7.1.0)
actionview_precompiler (>= 0.4)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)

Expand All @@ -120,6 +121,8 @@ GEM
specs:
action_text-trix (2.1.19)
railties
actionview_precompiler (0.4.0)
actionview (>= 6.0.a)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
allocation_stats (0.1.5)
Expand Down Expand Up @@ -486,6 +489,7 @@ CHECKSUMS
actionpack (8.2.0.alpha)
actiontext (8.2.0.alpha)
actionview (8.2.0.alpha)
actionview_precompiler (0.4.0) sha256=33b6bd6ec4c1b856e02fdf5f6512c9eb4a92ac1c0545e941b3e354b7d540ed1c
activejob (8.2.0.alpha)
activemodel (8.2.0.alpha)
activerecord (8.2.0.alpha)
Expand Down
4 changes: 4 additions & 0 deletions gemfiles/rails_main_head.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ PATH
specs:
view_component (4.12.0)
actionview (>= 7.1.0)
actionview_precompiler (>= 0.4)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)

Expand All @@ -120,6 +121,8 @@ GEM
specs:
action_text-trix (2.1.19)
railties
actionview_precompiler (0.4.0)
actionview (>= 6.0.a)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
allocation_stats (0.1.5)
Expand Down Expand Up @@ -456,6 +459,7 @@ CHECKSUMS
actionpack (8.2.0.alpha)
actiontext (8.2.0.alpha)
actionview (8.2.0.alpha)
actionview_precompiler (0.4.0) sha256=33b6bd6ec4c1b856e02fdf5f6512c9eb4a92ac1c0545e941b3e354b7d540ed1c
activejob (8.2.0.alpha)
activemodel (8.2.0.alpha)
activerecord (8.2.0.alpha)
Expand Down
1 change: 1 addition & 0 deletions lib/view_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module ViewComponent
autoload :CompileCache
autoload :Config
autoload :Deprecation
autoload :ExperimentallyCacheable
autoload :InlineTemplate
autoload :Instrumentation
autoload :Preview
Expand Down
109 changes: 109 additions & 0 deletions lib/view_component/cache_digestor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

require "digest"
require "view_component/template_dependency_extractor"

module ViewComponent

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how much of this work we could do at compile time, especially when eager loading is enabled. I believe everything except for the value(s) of cache_on should be stable at that point.

class CacheDigestor
def self.digest(component)
new(component: component).digest
end

def initialize(component:)
@component_class = component.is_a?(Class) ? component : component.class
@digests = {}
@file_cache = {}
@constant_cache = {}
end

def digest
digest_for_component(@component_class)
end

private

# Prevents infinite recursion when components render each other cyclically.
IN_PROGRESS = :__vc_in_progress

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This symbol is not referenced anywhere and there does not appear to be a test for this functionality. At a minimum, can you add a code comment clearly explaining its purpose?

private_constant :IN_PROGRESS

def digest_for_component(component_class)
return "" unless component_class <= ViewComponent::Base
name = component_class.name || component_class.object_id

cached_digest = @digests[name]
return "" if cached_digest == IN_PROGRESS
return cached_digest if cached_digest

@digests[name] = IN_PROGRESS

digest = Digest::SHA1.new

update_digest(digest, cached_file_contents(component_class.identifier))

inline_template = component_class.__vc_inline_template
if inline_template
inline_source = inline_template.source
update_digest(digest, inline_source)
update_template_dependency_digests(digest, inline_source, inline_template.language, component_class.identifier)
end

component_class.sidecar_files(ActionView::Template.template_handler_extensions).sort.each do |path|
template_source = cached_file_contents(path)
update_digest(digest, template_source)
update_template_dependency_digests(digest, template_source, File.extname(path).delete_prefix("."), path)
end

component_class.sidecar_files(%w[yml yaml]).sort.each do |path|
update_digest(digest, cached_file_contents(path))
end

@digests[name] = digest.hexdigest
end

def update_template_dependency_digests(digest, template_source, handler, identifier)
return unless template_source&.include?("render")

dependencies = ViewComponent::TemplateDependencyExtractor.new(template_source, handler, identifier: identifier).extract
update_dependency_digests(digest, dependencies)
end

def update_dependency_digests(digest, dependencies)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this method is only called on 65, let's inline it.

dependencies.each do |dep|
next unless uppercase_constant?(dep)

klass = cached_constantize(dep)
next unless klass

update_digest(digest, digest_for_component(klass))
end
end

def update_digest(digest, value)
return unless value

digest.update(value)
digest.update("\n")
end

def uppercase_constant?(dep)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, I'd mildly prefer to see this inlined.

return false unless dep

first = dep.getbyte(0)
first && first >= 65 && first <= 90
end

def cached_constantize(constant_name)
@constant_cache.fetch(constant_name) do
@constant_cache[constant_name] = constant_name.safe_constantize
end
end

def cached_file_contents(path)
return nil if path.nil?

@file_cache.fetch(path) do
@file_cache[path] = File.file?(path) ? File.read(path) : nil
end
end
end
end
Loading
Loading