Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 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
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
14 changes: 9 additions & 5 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*

* Add `protocol` parameter to `with_request_url` test helper to enable testing with HTTPS protocol.

*Joel Hawksley*
Expand Down Expand Up @@ -546,7 +550,7 @@ This release makes the following breaking changes:

## 3.23.0

* Add docs about Slack channel in Ruby Central workspace. (Join us! #oss-view-component). Email [email protected] for an invite.
* Add docs about Slack channel in Ruby Central workspace. (Join us! #oss-view-component). Email <[email protected]> for an invite.

*Joel Hawksley

Expand Down Expand Up @@ -1906,7 +1910,7 @@ Run into an issue with this release? [Let us know](https://github.com/ViewCompon

*Joel Hawksley*

* The ViewComponent team at GitHub is hiring! We're looking for a Rails engineer with accessibility experience: [https://boards.greenhouse.io/github/jobs/4020166](https://boards.greenhouse.io/github/jobs/4020166). Reach out to [email protected] with any questions!
* The ViewComponent team at GitHub is hiring! We're looking for a Rails engineer with accessibility experience: [https://boards.greenhouse.io/github/jobs/4020166](https://boards.greenhouse.io/github/jobs/4020166). Reach out to <[email protected]> with any questions!

* The ViewComponent team is hosting a happy hour at RailsConf. Join us for snacks, drinks, and stickers: [https://www.eventbrite.com/e/viewcomponent-happy-hour-tickets-304168585427](https://www.eventbrite.com/e/viewcomponent-happy-hour-tickets-304168585427)

Expand Down Expand Up @@ -2658,7 +2662,7 @@ Run into an issue with this release? [Let us know](https://github.com/ViewCompon

*Matheus Richard*

* Are you interested in building the future of ViewComponent? GitHub is looking to hire a Senior Engineer to work on Primer ViewComponents and ViewComponent. Apply here: [US/Canada](https://github.com/careers) / [Europe](https://boards.greenhouse.io/github/jobs/3132294). Feel free to reach out to [email protected] with any questions.
* Are you interested in building the future of ViewComponent? GitHub is looking to hire a Senior Engineer to work on Primer ViewComponents and ViewComponent. Apply here: [US/Canada](https://github.com/careers) / [Europe](https://boards.greenhouse.io/github/jobs/3132294). Feel free to reach out to <[email protected]> with any questions.

*Joel Hawksley*

Expand All @@ -2676,7 +2680,7 @@ Run into an issue with this release? [Let us know](https://github.com/ViewCompon

## 2.31.0

_Note: This release includes an underlying change to Slots that may affect incorrect usage of the API, where Slots were set on a line prefixed by `<%=`. The result of setting a Slot shouldn't be returned. (`<%`)_
*Note: This release includes an underlying change to Slots that may affect incorrect usage of the API, where Slots were set on a line prefixed by `<%=`. The result of setting a Slot shouldn't be returned. (`<%`)*

* Add `#with_content` to allow setting content without a block.

Expand Down Expand Up @@ -3124,7 +3128,7 @@ _Note: This release includes an underlying change to Slots that may affect incor

* The gem name is now `view_component`.
* ViewComponent previews are now accessed at `/rails/view_components`.
* ViewComponents can _only_ be rendered with the instance syntax: `render(MyComponent.new)`. Support for all other syntaxes has been removed.
* ViewComponents can *only* be rendered with the instance syntax: `render(MyComponent.new)`. Support for all other syntaxes has been removed.
* ActiveModel::Validations have been removed. ViewComponent generators no longer include validations.
* In Rails 6.1, no monkey patching is used.
* `to_component_class` has been removed.
Expand Down
45 changes: 45 additions & 0 deletions docs/guide/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
layout: default
title: Caching
parent: How-to guide
---

# Caching

Experimental
{: .label }

Caching is experimental.

To enable caching, include `ViewComponent::ExperimentallyCacheable`.

Components implement caching by marking dependencies using `cache_on`:

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

cache_on :foo, :bar
attr_reader :foo, :bar

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

```erb
Copy link
Member

Choose a reason for hiding this comment

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

If you want to include this code block, I'd probably put it above the notes section.

<p><%= view_cache_dependencies.inspect %></p>

<p><%= Time.zone.now %></p>
<p><%= "#{foo} #{bar}" %></p>
```

`cache_on` accepts method names. Returned values are expanded via `ActiveSupport::Cache.expand_cache_key`, so Active Record models, `GlobalID`, arrays, and plain strings work as expected.

Methods listed in `cache_on` may be private.

The cache key includes a digest of component source (Ruby + templates + i18n sidecars) and rendered child ViewComponents.

Partial/layout string dependencies are not currently included in the digest; modify `RAILS_CACHE_ID`/`RAILS_APP_VERSION` to invalidate on deploy.

Check warning on line 45 in docs/guide/caching.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Microsoft.Semicolon] Try to simplify this sentence. Raw Output: {"message": "[Microsoft.Semicolon] Try to simplify this sentence.", "location": {"path": "docs/guide/caching.md", "range": {"start": {"line": 45, "column": 76}}}, "severity": "INFO"}

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

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Microsoft.Contractions] Use 'aren't' instead of 'are not'. Raw Output: {"message": "[Microsoft.Contractions] Use 'aren't' instead of 'are not'.", "location": {"path": "docs/guide/caching.md", "range": {"start": {"line": 45, "column": 36}}}, "severity": "ERROR"}
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
3 changes: 1 addition & 2 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def config
include Rails.application.routes.url_helpers if defined?(Rails.application.routes)
include ERB::Escape
include ActiveSupport::CoreExt::ERBUtil

include ViewComponent::InlineTemplate
include ViewComponent::Slotable
include ViewComponent::Translatable
Expand All @@ -68,7 +67,7 @@ def config
# For Content Security Policy nonces
delegate :content_security_policy_nonce, to: :helpers

# Config option that strips trailing whitespace in templates before compiling them.
# Config:attr_writer :attr_names option that strips trailing whitespace in templates before compiling them.
class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false, default: false

attr_accessor :__vc_original_view_context
Expand Down
107 changes: 107 additions & 0 deletions lib/view_component/cache_digestor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

require "digest"
require "view_component/template_dependency_extractor"

module ViewComponent
class CacheDigestor
def initialize(component:)
@component = component
@digests = {}
@file_cache = {}
@constant_cache = {}
end

def digest
digest_for_component(@component.class)
end

private

IN_PROGRESS = :__vc_in_progress
private_constant :IN_PROGRESS

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

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, 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)
end

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

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

@digests[name] = digest.hexdigest
end

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

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

def update_dependency_digests(digest, dependencies)
dependencies.each do |dep|
next unless uppercase_constant?(dep)

klass = 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)
return false unless dep

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

def constantize(constant_name)
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this built-in to Rails?

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

def 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
20 changes: 20 additions & 0 deletions lib/view_component/cache_registry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module ViewComponent
module CachingRegistry
extend self

def caching?
ActiveSupport::IsolatedExecutionState[:view_component_caching] ||= false
end

def track_caching
caching_was = ActiveSupport::IsolatedExecutionState[:view_component_caching]
ActiveSupport::IsolatedExecutionState[:view_component_caching] = true

yield
ensure
ActiveSupport::IsolatedExecutionState[:view_component_caching] = caching_was
end
end
end
12 changes: 10 additions & 2 deletions lib/view_component/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,21 @@ def define_render_template_for
safe_call = template.safe_method_name_call
@component.define_method(:render_template_for) do |_|
@current_template = template
instance_exec(&safe_call)
if is_a?(ViewComponent::ExperimentallyCacheable)
__vc_render_cacheable(safe_call)
else
instance_exec(&safe_call)
end
end
else
compiler = self
@component.define_method(:render_template_for) do |details|
if (@current_template = compiler.find_templates_for(details).first)
instance_exec(&@current_template.safe_method_name_call)
if is_a?(ViewComponent::ExperimentallyCacheable)
__vc_render_cacheable(@current_template.safe_method_name_call)
else
instance_exec(&@current_template.safe_method_name_call)
end
else
raise MissingTemplateError.new(self.class.name, details)
end
Expand Down
Loading
Loading