Permalink

Comment #16

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.
Around this reply

More discussion

View full thread
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.
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,
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
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.
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
Daniel BrooksFull-stack developer
replying to #18
The piece I haven't seen anyone mention is what happens to your Storefront API rate limits once caching does its job. A high hit ratio concentrates every miss into the same window, so a cache flush or a cold deploy sends a burst of identical product and variant queries upstream at once, and Shopify's cost-based throttling will start returning `THROTTLED` errors precisely when traffic is highest. What helped on a recent build was coalescing in-flight requests: a small module that dedupes concurrent loaders asking for the same handle and shares one promise, plus a stale-while-revalidate fallback that serves the old payload rather than blocking on a retry. That turns a thundering herd into a single query, though it does mean owning some request-scoped state that Oxygen's runtime doesn't give you for free.
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.
AnonymousCommunity member
replying to #19
The thundering-herd fix has a nasty second-order effect on cost-based throttling: once you coalesce in-flight loaders, a single malformed query can stall every request waiting on that shared promise, so the failure blast radius grows with the dedupe benefit. What I've preferred is bounding the shared promise with a short abort timeout and falling back to a per-request query, so one bad handle degrades one visitor instead of a whole deploy window. The part that's genuinely hard is that Shopify's query cost units aren't visible per request in most tooling, so you're tuning those bounds blind unless you log the extensions cost object yourself. Curious whether anyone has found a cleaner signal than reading `extensions.cost` off each response.
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.