A cached shell is not a fresh price
Partial prerendering and explicit `use cache` are worth adopting: you get a fast shell without making the whole route dynamic. The trap is treating a product page as one cacheable thing. Catalog copy and sellable state do not share a freshness budget, and a tag you never invalidate is just a longer TTL with a comforting name.
What Cache Components actually changed
In the older App Router defaults, it was easy to cache by accident and hard to explain why a page was stale. Next.js 16's Cache Components flip that: data stays dynamic unless you mark it with `use cache`, and Partial Prerendering gives you a static shell with Suspense holes for the parts that must run per request.
That is a better default for storefronts and CMS-driven marketing sites. Editorial blocks, hero copy, and clinician resource pages can live in a tagged cache. The cart, account session, and anything that decides whether a SKU can be bought should not.
The API surface that matters day to day is small: `use cache` to opt in, `cacheTag` to name what you cached, `cacheLife` as a safety net TTL, and `revalidateTag` / `updateTag` when an upstream system says the world changed.
Two freshness budgets on one URL
A product page is usually two products glued together. One is content: title, description, care instructions, SEO fields, maybe a Sanity singleton the marketing team edits weekly. The other is commerce state: price, compare-at, variant availability, and whether the platform will actually accept the order.
Those change on different clocks. A clinician FAQ edit should not force a rebuild of inventory. A Shopify price change should not wait for the next CMS publish. If both reads sit behind one `use cache` function — or worse, an untagged one — you have already decided they share a fate.
On Provale-style builds the seam is familiar: Sanity owns explanation, Shopify owns money and the order. Caching should respect the same line. Cache the pages that teach. Keep the numbers that sell closer to the request, or on a short life with tags you can actually fire.
A tag you never call is not a plan
We have seen the tidy-looking version of this bug: every fetch wrapped in `use cache`, a few `cacheTag('products')` calls, and no webhook path that ever hits `revalidateTag`. The site feels fast in staging because nothing changes. In production, a price update lands in Shopify and the storefront keeps serving the warm entry until a TTL expires — if one was set at all.
Tag design is the real work. Prefer tags you can invalidate from an event you already receive: `product:{id}`, `collection:{handle}`, `page:{slug}`, `home`. Broad tags like `everything` make invalidation cheap to write and expensive to run. Missing tags make invalidation a no-op that looks successful in the route handler logs.
Pair tags with a fallback `cacheLife`. Webhooks get dropped, signatures fail, and deploys clear some caches and not others depending on the handler. A minutes-or-hours ceiling is the difference between a missed webhook and a weekend of wrong prices.
Wire invalidation to the systems that write
For Sanity, the publish webhook should map document type and ID to tags, verify the secret, then call `revalidateTag` with a stale-while-revalidate profile for editorial content. The notes schedule on this site is already a query filter on `publishedAt` — caching the listing without a tag for notes is how a scheduled post stays invisible after its slot opens.
For Shopify, product and inventory webhooks are the writers. Invalidate the specific product tag on `products/update` and the collection tags that include it. Do not revalidate the whole storefront because one SKU changed. And do not expose `/api/revalidate` without verifying the webhook — an open purge endpoint is a denial-of-service button.
When the user themselves just changed something in a Server Action — skip, pause, update address — prefer `updateTag` so the next render reads their write. Background `revalidateTag` is for editors and platforms. Read-your-writes is for the person who clicked.
What belongs in the shell
Put stable chrome and shared catalog framing in the prerendered shell: navigation, layout, long-lived marketing sections, imagery that is not price-dependent. Stream or request-render the holes that encode commitment: price, stock badge, subscription options, customer-specific B2B pricing.
If a number appears in the static HTML, treat it as a claim you are willing to defend until the next invalidation. That is the same standard we use for server-rendered stats: zeros and stale prices both look intentional. Fast and wrong still reads as a claim.
When in doubt during scoping, ask which field, if wrong for an hour, creates a support ticket. Those fields get short lives or no shared cache. Everything else can be tagged and warm.
A short checklist before you enable cacheComponents
List every cached function and the tag it declares. If you cannot name the webhook or action that clears the tag, do not ship the cache.
Split content reads from commerce reads at the data layer, not with a comment in a page component. Two functions, two tag namespaces, two lifetimes.
Add one test or manual script that updates an upstream field and asserts the storefront tag path runs — signature check included. Caching without a rehearsal of invalidation is just hoping the TTL is short enough.
Questions
- Should product prices use the same `use cache` boundary as CMS copy?
- No. Cache editorial and catalog explanation with tags tied to CMS publishes. Keep price, availability, and checkout- readiness on a short cacheLife, a separate tag namespace, or request-time rendering — they change on the commerce platform's clock.
- Is `cacheTag` enough without `revalidateTag`?
- A tag that nothing ever invalidates is only a label. Wire Sanity and Shopify (or your PIM) webhooks to verified revalidation, and keep a fallback cacheLife so a missed webhook cannot freeze stale sellable state.
- When should I use `updateTag` instead of `revalidateTag`?
- Use `updateTag` in Server Actions when the same user must see their own write on the next render. Use `revalidateTag` for editor and platform webhooks where stale-while-revalidate is acceptable.