Skip to content

Caching Architecture

Every page load traverses up to four cache tiers. Each tier eliminates a different class of work: browser I/O, edge CDN round-trips, per-section rendering, and Shopify origin fetches.

BROWSER CLOUDFLARE EDGE ORIGIN
─────── ─────────────── ──────
1. SW Prefetch ─miss─▶ 2. Dispatch CDN ─miss─▶ 3. Section Cache ─miss─▶ render
(stale-while- (full-page, (per-section,
revalidate) anonymous GET) input-hashed)
4. Origin Proxy (fallback)
(Shopify HTML, 5m TTL)

Registered at /lk/sw.js, versioned per deploy (lk-prefetch-{deployVersion}).

A companion inline script observes viewport links via IntersectionObserver (200 px margin) and batches prefetch requests through the SW. Two-tier freshness:

AgeBehaviour
< 5 minServe cached, revalidate in background
5–30 minServe stale, network fetch queued
> 30 minEntry expired, fetch fresh

Prefetch strategy — deterministic URLs extracted at render time (zero extra KV calls):

  • Index pages: collection URLs + first 4 products per collection
  • Collection pages: first 6 product URLs + next page
  • Blog pages: first 6 article URLs
  • Max 10 URLs per page

Header: X-LK-SW: hit / stale / miss

Bypass: ?lk-fresh=1 skips SW entirely. Paths like /cart, /account, /checkout are never prefetched.

Invalidation: navigator.serviceWorker.controller.postMessage({type:'INVALIDATE'}) — or via window.lk.invalidate.sw() in debug mode. Old caches are cleaned on SW activation.

Key files: src/lib/sw-script.ts, src/lib/prefetch.ts

Full-page caching at the edge via Cloudflare Cache API. Only anonymous GET requests to renderable paths are eligible.

Cache key:

{origin}{path}{search}?__v={variantId}&__dv={deployVersion}

The __v parameter isolates AB variants; __dv isolates deploy versions.

Eligibility rules:

  • GET only — POST/PUT always bypass
  • No cart=* cookie (cart cookie = personalized = uncacheable)
  • Not /api/*, /__dev/*, or debug mode
  • Not immutable assets (/assets/*-{hash}.* — cached separately with max-age=31536000)

TTL: 24 hours (s-maxage=86400). Set-Cookie headers are stripped before storage.

Header: X-Cache: HIT / MISS / BYPASS

Invalidation: POST /api/cache/purge with Authorization: Bearer {token} + { paths: ["/products/snowboard"] }. Also forwards to the theme worker for section cache + KV eviction.

Key files: packages/cf-client-dispatch/src/worker.ts

Per-section caching using content-addressable keys. Static sections are cached; dynamic sections (cart, customer) always render fresh.

Cache key:

https://section-cache/{slug}/{sectionType}/{inputHash}

The input hash (FNV-1a 32-bit → 8-char hex) covers:

  • Section settings (sorted JSON)
  • Resource handle (product/collection slug)
  • Locale
  • Deploy version
  • Market handle (for localized pricing)

This is self-invalidating — changing any input changes the hash, producing a new cache key.

TTL: 24 hours. Writes are fire-and-forget (don’t block the response).

Debug attributes: When debugMode=true, section wrappers include data-lk-section="{type}" and data-lk-cached="true".

Header: X-LK-Sections: {cachedCount}/{totalCount} (e.g. 7/9)

Bypass: ?lk-fresh=1 (requires debug mode) or DISABLE_SECTION_CACHE=true env var.

Key files: src/render/section-cache.ts, src/render/page-renderer.ts

Fallback for templates not yet transpiled. Fetches Shopify-rendered HTML, caches it briefly so the actual transpiled page can replace it.

TTL: 5 minutes (s-maxage=300). Only 200 OK HTML responses are cached.

Header: X-LK-Cache: proxy-hit / proxy-miss

Bypass: skipCache=true (used by debug fresh mode).

Key files: src/middleware/cached-proxy.ts

HeaderSourceValues
X-LK-SWService Workerhit, stale, miss
X-CacheDispatch CDNHIT, MISS, BYPASS
X-LK-SectionsSection cache{cached}/{total}
X-LK-CacheTheme workerrender, proxy-hit, proxy-miss, origin
X-Rendered-ByTheme workerlayerkick, layerkick-proxy

Activate with ?lk-debug=1 (sets a 1-day cookie). Adds a console script and window.lk API:

window.lk.fresh() // Reload with ?lk-fresh=1 (bypasses all caches)
window.lk.invalidate.sw() // Clear SW prefetch cache
window.lk.invalidate.cdn(paths) // Purge CDN cache for paths
window.lk.sections() // List sections with cache status
window.lk.off() // Disable debug mode

Console output: [LK] render | sections 7/9 | deploy: abc123

Disable with ?lk-debug=0 (clears cookie).

Requires debug mode active. Bypasses every cache layer:

LayerEffect
SW PrefetchSkipped entirely
Dispatch CDNReturns X-Cache: BYPASS, sends X-LK-Fresh: 1 to theme worker
Section CachesectionCache set to undefined — all sections render fresh
Origin ProxyskipCache=true — fetches from Shopify origin

Purges the dispatch worker’s Cache API entries and forwards to the theme worker for section cache + KV eviction. This does NOT purge the Cloudflare CDN zone cache.

Webhook-driven: Shopify webhooks → cf-trk → POST /api/cache/purge with bearer token.

POST /api/cache/purge
Authorization: Bearer {CACHE_PURGE_TOKEN}
Content-Type: application/json
{
"paths": ["/products/snowboard", "/collections/all"],
"keys": ["osc:product:snowboard"]
}
  • paths — evicts dispatch CDN cache entries (Cache API delete by URL)
  • keys — evicts in-memory KV cache entries (next KV read will fetch fresh)

The Cloudflare CDN edge also caches responses (with s-maxage=86400 TTL). This is a separate cache from the worker Cache API. After deploying or fixing a production issue, you must also purge the CDN zone cache to ensure users get fresh content immediately.

Terminal window
# Purge by hostname
curl -X POST "https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hosts":["example.layerkick.com"]}'
# Or purge everything in the zone
curl -X POST "https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-d '{"purge_everything":true}'

Alternatively, purge from the Cloudflare Dashboard → Caching → Configuration → Purge Everything (or Custom Purge by hostname).

Section cache keys are content-addressed — any setting change produces a new hash, so stale entries are never served (they expire via TTL).

Deploy activates new SW version → old lk-prefetch-* caches deleted.

LayerFile
SW Prefetchpackages/cf-client-site/src/lib/sw-script.ts
Prefetch URLspackages/cf-client-site/src/lib/prefetch.ts
Dispatch CDNpackages/cf-client-dispatch/src/worker.ts
Section Cachepackages/cf-client-site/src/render/section-cache.ts
Page Rendererpackages/cf-client-site/src/render/page-renderer.ts
Origin Proxypackages/cf-client-site/src/middleware/cached-proxy.ts
Debug Modepackages/cf-client-site/src/lib/debug.ts
Cache Purgepackages/cf-client-site/src/worker.ts (/api/cache/purge)