Skip to content

fix(web): shrink GROQ query weight + gate SanityLive behind draft mode - #137

Open
damianrosellen1 wants to merge 3 commits into
fix/web-ttfb-clsfrom
fix/web-query-weight-live-gating
Open

fix(web): shrink GROQ query weight + gate SanityLive behind draft mode#137
damianrosellen1 wants to merge 3 commits into
fix/web-ttfb-clsfrom
fix/web-query-weight-live-gating

Conversation

@damianrosellen1

Copy link
Copy Markdown
Collaborator

Follow-up to #136, implementing the cross-repo audit findings (same audit as farbstudio) for this repo. Stacked on fix/web-ttfb-cls — retarget to main once #136 merges.

1. Dead query weight (3cf1317)

settingsBundleQuery (312 KB), siteNavQuery (79 KB), siteSettingsQuery (77 KB) and siteCookieBannerQuery (77 KB) were exported but never imported outside the queries folder — ~546 KB of GROQ template strings built on every server cold start. errorSettingsQuery embedded modulesQuery (77 of its 79 KB) although no error page renders modules.

  • String exports at module init: 1.20 MB → 0.46 MB
  • errorSettingsQuery: 78.9 KB → 1.8 KB
  • READMEs updated; ErrorSettingsDocument loses its dead modules field

2. Rich-text projection blow-up (c29df3d)

Unlike farbstudio (where contentRefs was the driver — ours is lean), the blow-up here was buildRichTextMediaQuery(3): it repeated the full module.media / module.carousel / module.contentRefs projections (~19 KB) per nesting level. @sanity/client POSTs anything over 14,800 chars and sanityFetch fires two fetches per query (sync-tags probe + data) → ~154 KB upload per page query per uncached render.

Module projections now live only at the top level of a rich-text body; nested module.text keeps resolving blocks + link marks through the full depth.

Query before after
richTextMediaQuery 58.7 KB 22.1 KB
homeQuery / pageBySlugQuery / workQuery 77.3 KB 40.7 KB (−47%)
projectBySlugQuery 63.6 KB 27.1 KB

Trade-off (deliberate): media modules embedded inside a nested text module arrive with raw stored fields only and aren't expected to render — top-level rich text remains the module surface. Verified: trimmed homeQuery returns the full document against the live dataset; /work/design still renders all five rich-text-embedded media figures; page HTML sizes unchanged.

3. SanityLive gated behind Draft Mode (f643caa)

<SanityLive /> mounted for every visitor once SANITY_API_READ_TOKEN was set: an SSE connection per tab + a revalidateSyncTagsAction server-action round trip per publish event per tab. It had to — sanityFetch cache entries carried only sanity:* sync tags (revalidate: false, 1y cacheLife), which nothing but a mounted SanityLive revalidates.

The fix makes the webhook the published-content invalidation path, then gates Live:

  • every sanityFetch wrapper tags its cache entry with the matching SANITY_CACHE_TAGS constant
  • /api/revalidate covers the shell singletons (siteNav + siteCookieBanner added to the allow-list; siteSettings/errorSettings now map to tags)
  • SanityLive mounts for Draft Mode — and, as a freshness fallback, whenever a token is set but no SANITY_REVALIDATE_SECRET is configured (webhook-less setups keep today's behavior)

Verified end-to-end on the production build: HMAC-signed webhook posts for page / siteNav / siteSettings / siteCookieBanner / errorSettingsrevalidated: true and pages re-serve fresh; a full browser page load performs zero requests to *.sanity.io (previously an SSE connection to /data/live/events). No token leak existed before (next-sanity only sends browserToken in draft mode) — the win is connection + action traffic. Client JS is unchanged: the remaining @sanity/client/stega chunks come from useOptimistic (next-sanity/hooks) in ModulesRendererClient, not from Live — possible follow-up.

Not done (deliberately)

Verified

  • pnpm typecheck + pnpm format clean, production build green, all routes still SSG/ISR
  • Query-size harness before/after (numbers above)
  • Live-dataset smoke tests: trimmed queries return full documents; embedded rich-text media renders
  • Webhook HMAC verification + tag mapping for all document types
  • Browser network capture: no sanity.io traffic on anonymous pages

🤖 Generated with Claude Code

damianrosellen1 and others added 3 commits July 28, 2026 14:23
…rojection

settingsBundleQuery (312 KB), siteNavQuery (79 KB), siteSettingsQuery
(77 KB) and siteCookieBannerQuery (77 KB) were exported but never
imported outside the queries folder — settings.ts itself marked the
bundle 'safe to delete if unused'. Together they built ~546 KB of GROQ
template strings on every server cold start for nothing.

errorSettingsQuery embedded modulesQuery (77 of its 79 KB) although no
error page renders modules — LocaleNotFoundContent reads only
notFoundTitle/notFoundBody. Dropping the projection shrinks the query
to 1.8 KB; the hand type loses its dead modules field.

Query-size harness (esbuild compile of the query modules):
- errorSettingsQuery 78.9 KB -> 1.8 KB
- string exports at module init 1.20 MB -> 0.46 MB

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildRichTextMediaQuery repeated the full module.media / module.carousel
/ module.contentRefs projections (~19 KB) on every one of its three
nesting levels — ~57 KB for the snippet alone and ~77 KB per page query.
@sanity/client POSTs anything over 14,800 chars and next-sanity's
sanityFetch issues two fetches per query (sync-tags probe + data), so
every uncached render uploaded ~154 KB of GROQ text per page query.

Module projections now live only on the top level of a rich-text body.
Nested module.text bodies keep resolving blocks + link marks through the
full depth; media modules inside a *nested* text module arrive with raw
stored fields only and are not expected to render — top-level rich text
remains the module surface.

Query-size harness:
- richTextMediaQuery 58.7 KB -> 22.1 KB
- homeQuery / pageBySlugQuery / workQuery 77.3 KB -> 40.7 KB (−47%)
- projectBySlugQuery 63.6 KB -> 27.1 KB

Verified against the live dataset: trimmed homeQuery returns the full
document (200, modules intact); /work/design still renders all five
rich-text-embedded media figures; page HTML sizes unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e tags

<SanityLive /> mounted for every visitor as soon as SANITY_API_READ_TOKEN
was set (any deploy with Visual Editing): one SSE connection per open tab
plus a revalidateSyncTagsAction server-action round trip per publish event
per tab. It had to — the sanityFetch data-cache entries carried only
next-sanity's sanity:* sync tags (revalidate: false, 1y cacheLife), which
nothing but a mounted SanityLive ever revalidates.

- fetchSanityData: every sanityFetch wrapper now also tags its cache
  entry with the matching SANITY_CACHE_TAGS constant, so the existing
  /api/revalidate webhook invalidates page and shell data directly
- /api/revalidate: siteNav + siteCookieBanner join the allow-list;
  siteSettings, siteNav, siteCookieBanner, errorSettings map to their
  new tags (kept in sync with SANITY_CACHE_TAGS)
- layout: SanityLive mounts for Draft Mode (Presentation live preview),
  and — as a freshness fallback — whenever a read token is set but no
  SANITY_REVALIDATE_SECRET is configured, so webhook-less setups keep
  the previous behavior

Verified against the running production build: HMAC-signed webhook posts
for page/siteNav/siteSettings/siteCookieBanner/errorSettings revalidate
their tags (200, revalidated: true) and the page re-serves fresh; a full
browser page load makes zero requests to *.sanity.io (previously an SSE
connection to /data/live/events). Draft-mode requests still mount Live.

Note: client JS is unchanged — the @sanity/client/stega chunks on public
pages come from useOptimistic (next-sanity/hooks) in ModulesRendererClient,
not from SanityLive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant