ddtcorex

06 Sept 2026 · 3 min read

Case study: full-page cache broke the category page's JavaScript

A production Hyva storefront loaded fine on the first hit and broke on the second. The culprit was FPC caching a fragment without the JS blocks it depended on.

Field notes — a real production bug, project name withheld. Symptom → investigation → fix → lesson.

The symptom

On a Hyva storefront, the category page worked on the first visit and fell apart on the second. The grid rendered, but its interactive pieces were dead: the wishlist toggle did nothing, the compare control was unbound, and the price box never updated. The difference between the two requests was one thing — the second response was served from the full-page cache.

On a cache miss the page was fully interactive. On a cache hit the same markup came back but the JavaScript that wired those controls up was nowhere to be found.

Investigation

The category list item is a Hyva block that registers its JavaScript through hyva_js_block_dependencies. In the original layout, the wishlist, compare, price-box, and item JS blocks were declared as dependencies of the product list renderer, with every flag set to true:

<argument name="hyva_js_block_dependencies" xsi:type="array">
    <item name="category.products.list.js.wishlist" xsi:type="boolean">true</item>
    <item name="category.products.list.js.compare" xsi:type="boolean">true</item>
    <item name="category.products.list.js.item" xsi:type="boolean">true</item>
    <item name="category.products.list.js.price.box" xsi:type="boolean">true</item>
</argument>

That mechanism works when the block is rendered through the normal Hyva render pipeline on every request. But FPC captures the rendered HTML once and replays it. If the dependency resolution that injects the JS runs only during the initial render — not during the cached replay — the cached fragment is missing the scripts, and the second visitor gets a dead page.

Root cause

The JS blocks were bound to the product-list renderer as dependencies, so they only accompanied that block when it was rendered live. Once FPC served the list from cache, the dependency stage did not run, and the scripts that the cached HTML referenced never got emitted. The cache hit was fast but incomplete.

The fix

Move the JS blocks out of the dependency list and render them directly into before.body.end, where they are part of the page output regardless of how the list fragment was produced. Flip the dependency flags to false so Hyva no longer expects to inject them through the renderer:

<argument name="hyva_js_block_dependencies" xsi:type="array">
    <item name="category.products.list.js.wishlist" xsi:type="boolean">false</item>
    <item name="category.products.list.js.compare" xsi:type="boolean">false</item>
    <item name="category.products.list.js.item" xsi:type="boolean">false</item>
    <item name="category.products.list.js.price.box" xsi:type="boolean">false</item>
</argument>
<referenceContainer name="before.body.end">
    <block name="acme.product.list.js.wishlist"
           template="Magento_Catalog::product/list/js/wishlist.phtml" />
    <block name="acme.product.list.js.compare"
           template="Magento_Catalog::product/list/js/compare.phtml" />
    <block name="acme.product.list.js.price.box"
           template="Magento_Catalog::product/list/js/price-box.phtml" />
    <block name="acme.product.list.js.item"
           template="Magento_Catalog::product/list/js/item.phtml" />
</referenceContainer>

The cached category HTML now always ships with the scripts it needs.

Lesson

FPC is not free interactivity. Any JavaScript that must be present on a cached page has to live in a container that is emitted as part of the served output, not injected lazily through a render-time dependency. The cheapest verification is the one we used: fetch the page twice, confirm the second response is a cache hit (Age/CF-Cache-Status or your cache header), then confirm in a browser that the controls still work. A cache that strips a script tag looks fast and is broken.

The flip side also holds — the checkout and any Magewire/React fragment must be excluded or hole-punched, or the cache will serve a stale, non-functional page there instead.

magento2hyvaperformancefpc