Permalink

Comment #14

Sophie EvansUI developer
replying to #13
Cached HTML also freezes accessibility semantics in ways that are easy to miss. If a loader formats prices or dates for `localization.country` server-side, a German shopper can end up with a cached English date string, and screen readers announce it with the wrong language because the `lang` attribute on that fragment was rendered alongside it. The same thing happens with `dir` on RTL markets. What works better is emitting a neutral, machine-readable value with an explicit `lang` and letting a tiny client formatter handle the locale-specific
Around this reply

More discussion

View full thread
Leo MartinWeb developer
replying to #12
The debugging side of this deserves its own thought, because cached HTML breaks the usual assumption that what your logs show is what a visitor saw. Accidentally embedding a render timestamp in a server-rendered component is the classic version: the HTML gets cached with that moment frozen, and it quietly confirms the wrong build is still live
Nora PatelCommerce developer
replying to #14
Sophie's point about baked-in locale formatting connects to something bigger: cached HTML freezes the *entire* accessible name of a control, not just its text. If your variant picker renders `aria-label="Size Medium, out of stock"` server-side from a Storefront API availability read, that label ships inside the cached response even though another visitor hits the same product with Medium in stock. The shopper sees a selectable swatch, but a screen reader announces it as unavailable until hydration corrects it. I've started passing raw availability into the markup and composing the label in the component after the selection query resolves, keeping the cached shell purely structural. It costs a flicker in announced state, which is arguably worse, so I'm genuinely unsure which failure mode to prefer here.
Maya ChenFrontend developer
replying to #11
Variant data has the same shape problem but in reverse. Pulling every option combination into the collection loader inflates the payload and, worse, bakes availability into cached HTML that goes stale the moment one SKU sells out. What has worked better for me is a dedicated selection query that runs on demand once a shopper actually picks a combination, keyed by product handle plus selected options and served with a short TTL, so the collection page never carries variant arrays it won't render. That shifts the tradeoff to a small client round trip at selection time, which is usually invisible, and keeps the bulk of the page cacheable with boring keys. The annoyance is mapping Shopify's option position semantics onto a flat key you can actually cache.
Marcus ReedEdge developer
replying to #15
That availability-label problem has an observability cousin: once the shell is cached, your synthetic checks can pass green while a real shopper sees the wrong announced state, because the monitor hydrates and the edge visitor may not. I have had better luck emitting a structured `data-availability` attribute plus a stable `aria-describedby` pointing at a separate live region the selection query owns, so the accessible name never depends on a cached read. Cloudflare Logpush or Oxygen's log stream can then tag responses by cache status and compare error rates between HIT and MISS, which is the only way I have found to tell a hydration regression from a genuine availability bug.
Nora PatelCommerce developer
replying to #10
There's a related trap in how product data itself gets shaped for faceted navigation. If collection loaders pull full variant arrays just to compute which filter values exist, every facet change drags a payload the page never renders, and stale facet counts linger in cache alongside the products. I've had better luck fetching filter metadata through a separate query keyed by collection handle and a short TTL, then letting product results stay lean and long-lived. Search analytics from Shopify's Search & Discovery app also feed back into which facets are worth surfacing, so the query design and the merchandising config drift apart fast unless someone owns both.
Leo MartinWeb developer
replying to #16
Marcus's HIT/MISS split is exactly right, though I'd add that hit ratio alone can go *up* while you're serving more wrong responses, which is the scary direction. On a recent build with `@defer` on the Storefront API,
Marcus ReedEdge developer
replying to #9
Leo, that variant extends further into anything the loader derives from time rather than identity. We served a promotional banner from a loader that checked whether a sale window had passed, and because the response carried a long edge TTL, shoppers kept seeing expired pricing well after the cutoff. The fix was keeping the decision out of the cached HTML entirely: render a neutral shell, then have a tiny client fetch resolve the promotion state against a short-lived endpoint. The same pattern handles delivery estimates and stock messaging, since those depend on clocks and warehouse state that no cache key can capture. Worth noting that cache-hit ratio and time-to-first-byte observability need to be tracked separately from loader latency, otherwise a stale-content incident looks like a slow origin.
Sophie EvansUI developer
replying to #17
The @defer angle is interesting because deferred fragments can land after first paint, which means the announced state on a cached shell and the eventual state can diverge in ways no cache-status log distinguishes. What I've started doing for variant pickers is rendering every option as a real `<button>` with `aria-disabled` from a client selection query rather than removing or enabling it server-side, so the tab order stays stable across HIT and MISS and a shopper navigating by keyboard never loses their place mid-hydration. The tradeoff is that all combinations must exist in the cached markup, which contradicts lean collection payloads, so I split: lean collection cards, fuller shell only on the product route itself.
Join the discussion

Leave a comment

Name and message are required. Website is optional. Public website links use rel="ugc nofollow" to reduce comment spam.