Add element-level opt-out for page caching - #3980
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3980 +/- ##
==========================================
+ Coverage 98.26% 98.27% +0.01%
==========================================
Files 350 351 +1
Lines 9184 9241 +57
==========================================
+ Hits 9025 9082 +57
Misses 159 159 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
19a8a0b to
e5c19c5
Compare
tvdeyen
left a comment
There was a problem hiding this comment.
I like that feature. Would you mind to fix the remaining specs? We plan to make significant changes to the repo and this will break this PR. Also we plan to release 8.4 soon and we would include in that release
Allow element definitions to declare page_cache: false so pages that contain or can render those elements skip HTTP page-shell caching. Use no-store for this opt-out because no-cache still permits stored responses and conditional revalidation, which can return 304 before element-level cache variants are rendered. Keep the option as definition-only metadata and cover the new behavior with model and request specs.
e5c19c5 to
8572837
Compare
Fixed. The expectation was wrong. The dynamically created |
tvdeyen
left a comment
There was a problem hiding this comment.
Nice feature — the no-store reasoning is spot on. 👍 One thing to flag: page_cache_disabled_by_elements? runs on every show/index, and because it calls @page.find_elements, it now loads and instantiates the full published element set on every request — including conditional GETs that return 304. Previously a 304 only ran the lightweight EtagGenerator pluck and never materialized elements, so this adds real work to the hot cache path. It's also evaluated up to 3× per request (twice in set_expiration_headers, once via render_fresh_page?) with no memoization.
Two small things make it nearly free again:
- Short-circuit statically — in the common case no definition opts out, so we can skip the DB entirely; and when one does, an
exists?is far cheaper than loading full records (it also naturally covers nested/fixed elements, whichfind_elements' defaultvisible.not_nested.unfixedscope currently misses). - Memoize per request (guarding with
defined?since the result can befalse).
def page_cache_disabled_by_elements?
return @page_cache_disabled_by_elements if defined?(@page_cache_disabled_by_elements)
opt_out_names = Alchemy::Element.definitions.filter_map { |d| d.name if d.page_cache == false }
@page_cache_disabled_by_elements = !!(
opt_out_names.any? &&
@page&.public_version&.elements&.published&.exists?(name: opt_out_names)
)
endThis keeps behavior identical for the specs here, stays zero-cost for apps that don't use the opt-out, and closes the nested/fixed-element gap for free. 🙂
Closes #3973.
This adds a
page_cacheoption to element definitions, allowing individual elements to opt a page out of HTTP page caching:When a page contains an element definition with
page_cache: false, Alchemy now always renders the page instead of relying on the page-cache. This lets dynamic or permission-sensitive elements execute and apply their own fragment cache keys.The opt-out uses
Cache-Control: no-storerather than no-cache, because no-cache may still allow stored responses to be revalidated and return 304, which would skip rendering the element entirely.The option defaults to true and is kept as definition-only metadata, so existing element definitions and page cache behavior remain unchanged unless an element explicitly opts out.
Checklist