[{"data":1,"prerenderedAt":1101},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation":1093},{"id":4,"title":5,"body":6,"description":1071,"extension":1072,"meta":1073,"navigation":1086,"path":1087,"seo":1088,"stem":1091,"__hash__":1092},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation\u002Findex.md","SWR via Cache-Control vs Service Worker revalidation",{"type":7,"value":8,"toc":1057},"minimark",[9,13,33,46,51,54,132,139,143,146,187,191,202,211,409,413,553,557,560,565,568,608,614,618,621,821,834,838,841,885,893,897,900,905,912,921,925,928,998,1006,1010,1042,1047,1050,1053],[10,11,5],"h1",{"id":12},"swr-via-cache-control-vs-service-worker-revalidation",[14,15,16,17,22,23,27,28,32],"p",{},"This comparison sits within the ",[18,19,21],"a",{"href":20},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","stale-while-revalidate implementation"," guide under ",[18,24,26],{"href":25},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",", and resolves a recurring architecture question: you want stale-while-revalidate behaviour, but should it live as an HTTP ",[29,30,31],"code",{},"Cache-Control"," directive enforced at the CDN edge, or as logic inside a Service Worker on the device?",[14,34,35,36,40,41,45],{},"Both deliver the same user-facing promise — instant cached response now, fresh copy next time — but they operate at different layers, with different blast radius, freshness guarantees, and effects on ",[18,37,39],{"href":38},"\u002Fcore-web-vitals-measurement\u002Foptimizing-first-input-delay-fid\u002F","Interaction to Next Paint"," and ",[18,42,44],{"href":43},"\u002Fcore-web-vitals-measurement\u002Fmeasuring-lcp-with-chrome-devtools\u002F","Largest Contentful Paint",". The targets are the same either way: TTFB ≤ 200ms, LCP \u003C 2.5s, INP \u003C 200ms, and no main-thread task over the 50ms long-task budget. Pick the wrong layer and you either serve a returning user a week-old page, block the site behind a sticky worker you cannot recall, or spend INP budget on a background cache write. This page triages that choice.",[47,48,50],"h2",{"id":49},"rapid-diagnosis-which-layer-is-your-swr-actually-on","Rapid diagnosis: which layer is your SWR actually on?",[14,52,53],{},"Before choosing, confirm where staleness is being produced today — the two layers leave distinct fingerprints in DevTools:",[55,56,57,87,106,116,122],"ul",{},[58,59,60,64,65,68,69,68,72,75,76,79,80,82,83,86],"li",{},[61,62,63],"strong",{},"Open the Network panel and reload the route."," If the served response carries a CDN cache-status header (",[29,66,67],{},"cf-cache-status",", ",[29,70,71],{},"x-cache",[29,73,74],{},"age",") and no ",[29,77,78],{},"(ServiceWorker)"," label, your SWR is running at the edge. If the row is labelled ",[29,81,78],{}," and shows ",[29,84,85],{},"Size: (ServiceWorker)",", a worker answered it.",[58,88,89,92,93,96,97,100,101,105],{},[61,90,91],{},"Check Application → Service Workers."," A registered, ",[29,94,95],{},"activated"," worker with a ",[29,98,99],{},"fetch"," handler means the ",[102,103,104],"em",{},"device"," can intercept and serve stale independent of any edge header — this is the layer most teams forget is even live.",[58,107,108,111,112,115],{},[61,109,110],{},"Read the response headers."," ",[29,113,114],{},"Cache-Control: ...stale-while-revalidate=\u003Cn>"," in the response is the edge\u002Fshared-cache contract; its absence means any staleness you see is coming from the worker's Cache API, not HTTP.",[58,117,118,121],{},[61,119,120],{},"Throttle to Offline and reload."," A page that still paints proves a worker is serving from the local cache; a network error proves you are edge-only, with no on-device copy.",[58,123,124,127,128,131],{},[61,125,126],{},"Diff the body across two rapid reloads just after expiry."," Edge SWR updates for ",[102,129,130],{},"every"," visitor to that point of presence after one background refresh; worker SWR updates only on the specific device once its own revalidation completes.",[14,133,134,135,138],{},"That checklist tells you which layer owns freshness right now. The rest of this page decides which layer ",[102,136,137],{},"should",".",[47,140,142],{"id":141},"root-cause-why-the-wrong-layer-bites","Root cause: why the wrong layer bites",[14,144,145],{},"Four named failure modes account for almost every regret after shipping SWR at the wrong layer:",[147,148,149,159,165,178],"ol",{},[58,150,151,154,155,158],{},[61,152,153],{},"Per-device staleness divergence."," A Service Worker cache is private to one browser, so a worker chosen for frequently-read ",[102,156,157],{},"shared"," content serves each returning user their own stale copy — someone who has not visited in a week gets a week-old page no matter how many others refreshed. The mechanism is simple: there is no shared object to converge on freshness.",[58,160,161,164],{},[61,162,163],{},"Offline blank-out."," Edge SWR still requires a network hop to the nearest PoP; pick it for an installable app shell or a flaky-network audience and the first failed request paints nothing, because no copy lives on the device.",[58,166,167,170,171,174,175,177],{},[61,168,169],{},"Main-thread revalidation tax."," A worker's ",[29,172,173],{},"cache.put"," and response clone\u002Fparse run on the device event loop. A large synchronous body handled inline can produce a task over the 50ms long-task budget and regress ",[18,176,39],{"href":38}," — a cost the edge never imposes because its refetch happens off-device.",[58,179,180,183,184,186],{},[61,181,182],{},"Sticky-worker lock-in."," Once installed, a worker intercepts every request on the origin, including the request for its own replacement. A buggy ",[29,185,99],{}," handler can therefore make its own fix un-fetchable — the operational asymmetry that makes worker rollbacks the riskier of the two.",[47,188,190],{"id":189},"where-each-layer-revalidates","Where each layer revalidates",[14,192,193,194,197,198,201],{},"With ",[29,195,196],{},"Cache-Control: stale-while-revalidate",", the ",[102,199,200],{},"edge"," owns the logic. The first byte the browser receives still comes over the network, but from the nearest point of presence, and the edge — not the device — handles the background refetch from origin. It is declarative: one header, applied to every visitor of every PoP, with no client code.",[14,203,204,205,207,208,210],{},"With a Service Worker, the ",[102,206,104],{}," owns the logic. The worker intercepts ",[29,209,99],{},", answers from the Cache API with zero network on a hit, and issues its own background revalidation. It is imperative: JavaScript you ship and version, running per browser, and it works offline.",[14,212,213],{},[214,215,111,222,111,226,111,230,111,247,111,256,111,263,111,270,111,279,111,285,111,291,111,295,111,302,111,307,111,312,111,317,111,323,111,329,111,332,111,337,111,341,111,344,111,347,111,351,111,353,111,357,111,361,111,364,111,367,111,370,111,376,111,380,111,386,111,390,111,396,111,400,111,404,111],"svg",{"xmlns":216,"viewBox":217,"width":218,"role":219,"ariaLabel":220,"style":221},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 400","100%","img","Side-by-side request paths for stale-while-revalidate at the CDN edge versus in a device service worker, and who each layer serves stale","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:#001d3d",[223,224,225],"title",{},"How each SWR layer answers a request, and who it serves stale",[227,228,229],"desc",{},"Edge SWR reaches a shared cache over a network hop and heals for every visitor behind the point of presence after one background refetch; worker SWR reads a per-device cache with zero network and works offline, but a returning visitor who has been away a week is still served a week-old copy. Both revalidate the origin off the response path.",[231,232,111,233,111],"defs",{},[234,235,111,242,111],"marker",{"id":236,"viewBox":237,"refX":238,"refY":239,"markerWidth":240,"markerHeight":240,"orient":241},"swr-ar","0 0 10 10","8","5","7","auto-start-reverse",[243,244],"path",{"d":245,"fill":246},"M0 0 L10 5 L0 10 z","currentColor",[248,249],"rect",{"x":250,"y":250,"width":251,"height":252,"rx":253,"fill":254,"stroke":246,"style":255},"1","758","398","10","none","stroke-opacity:0.18",[257,258,262],"text",{"x":259,"y":260,"fill":246,"style":261},"24","32","font-size:15.5px;font-weight:700","How each layer answers a request — and who it serves stale",[264,265],"line",{"x1":266,"y1":267,"x2":266,"y2":268,"stroke":246,"style":269},"380","52","288","stroke-opacity:0.12",[248,271],{"x":272,"y":273,"width":274,"height":275,"rx":276,"fill":277,"stroke":277,"style":278},"40","50","320","30","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.5",[257,280,284],{"x":281,"y":282,"fill":246,"style":283},"200","70","font-size:13px;font-weight:600;text-anchor:middle","Cache-Control SWR · CDN edge",[248,286],{"x":287,"y":273,"width":274,"height":275,"rx":276,"fill":288,"stroke":289,"style":290},"400","#ffc300","#b8860b","fill-opacity:0.22;stroke-opacity:0.55",[257,292,294],{"x":293,"y":282,"fill":246,"style":283},"560","Service Worker SWR · on device",[248,296],{"x":297,"y":298,"width":299,"height":300,"rx":276,"fill":254,"stroke":246,"style":301},"120","98","160","42","stroke-opacity:0.55",[257,303,306],{"x":281,"y":304,"fill":246,"style":305},"118","font-size:12.5px;font-weight:600;text-anchor:middle","Browser",[257,308,311],{"x":281,"y":309,"fill":246,"style":310},"134","font-size:11px;text-anchor:middle;opacity:0.75","returning visitor",[264,313],{"x1":281,"y1":314,"x2":281,"y2":315,"stroke":246,"style":301,"markerEnd":316},"140","176","url(#swr-ar)",[257,318,322],{"x":319,"y":320,"fill":246,"style":321},"212","162","font-size:10.5px","network hop 20–80ms",[248,324],{"x":325,"y":326,"width":281,"height":327,"rx":276,"fill":277,"stroke":277,"style":328},"100","178","66","fill-opacity:0.12;stroke-opacity:0.5",[257,330,331],{"x":281,"y":281,"fill":246,"style":305},"CDN edge · shared cache",[257,333,336],{"x":281,"y":334,"fill":246,"style":335},"219","font-size:11px;text-anchor:middle","serves stale instantly",[257,338,340],{"x":281,"y":339,"fill":246,"style":335},"235","one refresh heals the PoP",[248,342],{"x":343,"y":298,"width":299,"height":300,"rx":276,"fill":254,"stroke":246,"style":301},"480",[257,345,306],{"x":293,"y":346,"fill":246,"style":305},"116",[257,348,350],{"x":293,"y":349,"fill":246,"style":310},"132","+ Service Worker",[264,352],{"x1":293,"y1":314,"x2":293,"y2":315,"stroke":246,"style":301,"markerEnd":316},[257,354,356],{"x":355,"y":320,"fill":246,"style":321},"572","zero network · same device",[248,358],{"x":359,"y":326,"width":281,"height":327,"rx":276,"fill":288,"stroke":289,"style":360},"460","fill-opacity:0.2;stroke-opacity:0.55",[257,362,363],{"x":293,"y":281,"fill":246,"style":305},"Cache API · per device",[257,365,366],{"x":293,"y":334,"fill":246,"style":335},"serves stale offline",[257,368,369],{"x":293,"y":339,"fill":246,"style":335},"week-away user still stale",[243,371],{"d":372,"fill":254,"stroke":246,"style":373,"strokeDashArray":374,"markerEnd":316},"M200 244 C200 285, 300 292, 340 314","stroke-opacity:0.5",[239,375],"4",[243,377],{"d":378,"fill":254,"stroke":246,"style":373,"strokeDashArray":379,"markerEnd":316},"M560 244 C560 285, 460 292, 420 314",[239,375],[257,381,385],{"x":382,"y":383,"fill":246,"style":384},"238","278","font-size:10.5px;text-anchor:middle","background refetch",[257,387,389],{"x":388,"y":383,"fill":246,"style":384},"512","background put · costs INP",[248,391],{"x":392,"y":393,"width":394,"height":395,"rx":276,"fill":254,"stroke":246,"style":301},"290","314","180","48",[257,397,399],{"x":266,"y":398,"fill":246,"style":305},"336","Origin",[257,401,403],{"x":266,"y":402,"fill":246,"style":310},"353","source of truth",[257,405,408],{"x":266,"y":406,"fill":246,"style":407},"384","font-size:11px;text-anchor:middle;opacity:0.8","Both revalidate off the response path — the difference is who holds the stale copy.",[47,410,412],{"id":411},"decision-matrix","Decision matrix",[414,415,416,432],"table",{},[417,418,419],"thead",{},[420,421,422,426,429],"tr",{},[423,424,425],"th",{},"Dimension",[423,427,428],{},"Cache-Control SWR (edge)",[423,430,431],{},"Service Worker SWR (device)",[433,434,435,447,458,469,480,491,509,520,531,542],"tbody",{},[420,436,437,441,444],{},[438,439,440],"td",{},"Layer",[438,442,443],{},"CDN \u002F shared cache",[438,445,446],{},"Per-device, in the browser",[420,448,449,452,455],{},[438,450,451],{},"Hit latency",[438,453,454],{},"Network hop to nearest PoP (~20-80ms)",[438,456,457],{},"Zero network — reads local Cache API",[420,459,460,463,466],{},[438,461,462],{},"Offline support",[438,464,465],{},"None — needs the network",[438,467,468],{},"Full — serves from cache offline",[420,470,471,474,477],{},[438,472,473],{},"Freshness control",[438,475,476],{},"Coarse: one TTL\u002Fwindow per route",[438,478,479],{},"Fine: arbitrary per-request logic",[420,481,482,485,488],{},[438,483,484],{},"Who is served stale",[438,486,487],{},"Every visitor on first request after expiry",[438,489,490],{},"Only the specific device, per its cache",[420,492,493,496,499],{},[438,494,495],{},"INP impact",[438,497,498],{},"None — work happens off-device",[438,500,501,502,504,505,508],{},"Slight — background ",[29,503,99],{},"\u002F",[29,506,507],{},"put"," on device",[420,510,511,514,517],{},[438,512,513],{},"LCP impact",[438,515,516],{},"Depends on PoP RTT",[438,518,519],{},"Best — instant on repeat visits",[420,521,522,525,528],{},[438,523,524],{},"First-visit benefit",[438,526,527],{},"Yes — shared cache helps cold clients",[438,529,530],{},"No — cache is empty until first fetch",[420,532,533,536,539],{},[438,534,535],{},"Setup complexity",[438,537,538],{},"Low — one header \u002F edge rule",[438,540,541],{},"High — register, version, debug a worker",[420,543,544,547,550],{},[438,545,546],{},"Failure blast radius",[438,548,549],{},"Edge config, easy to roll back",[438,551,552],{},"Buggy worker can break the whole site",[47,554,556],{"id":555},"step-by-step-resolution-ordered-by-impact","Step-by-step resolution, ordered by impact",[14,558,559],{},"Work these in order. Most sites finish at step 1; only add the later layers when a named failure mode above actually applies.",[561,562,564],"h3",{"id":563},"_1-default-to-cache-control-swr-at-the-edge","1. Default to Cache-Control SWR at the edge",[14,566,567],{},"Make the shared edge the baseline for HTML, API JSON, and any resource where a sub-100ms PoP response is good enough and you do not need offline. It benefits first-time and cold-cache visitors (a shared edge object is warm for everyone), needs no client JavaScript, and keeps the main thread free.",[569,570,575],"pre",{"className":571,"code":572,"language":573,"meta":574,"style":574},"language-http shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","# Origin\u002Fedge response — the CDN owns the background refresh.\nCache-Control: public, max-age=60, stale-while-revalidate=600\n# trade-off: the window is one coarse value for all visitors. You cannot\n# revalidate \"only when the user's permissions changed\" — for per-request\n# freshness logic you need the worker version in step 2, not this header.\n","http","",[29,576,577,584,590,596,602],{"__ignoreMap":574},[578,579,581],"span",{"class":264,"line":580},1,[578,582,583],{},"# Origin\u002Fedge response — the CDN owns the background refresh.\n",[578,585,587],{"class":264,"line":586},2,[578,588,589],{},"Cache-Control: public, max-age=60, stale-while-revalidate=600\n",[578,591,593],{"class":264,"line":592},3,[578,594,595],{},"# trade-off: the window is one coarse value for all visitors. You cannot\n",[578,597,599],{"class":264,"line":598},4,[578,600,601],{},"# revalidate \"only when the user's permissions changed\" — for per-request\n",[578,603,605],{"class":264,"line":604},5,[578,606,607],{},"# freshness logic you need the worker version in step 2, not this header.\n",[14,609,610,613],{},[61,611,612],{},"Expected outcome:"," TTFB holds ≤ 200ms across PoPs, the first visitor after expiry absorbs the single background refresh, and freshness converges for the whole audience behind that node within one revalidation cycle. Rollback is a one-line header revert that propagates in seconds.",[561,615,617],{"id":616},"_2-add-a-service-worker-layer-only-where-http-cannot-reach","2. Add a Service Worker layer only where HTTP cannot reach",[14,619,620],{},"Add worker SWR when you specifically need true offline support, zero-network repeat-visit reads (an installable PWA or app shell), or per-request revalidation logic too nuanced for a single header — for example revalidating only when a client-side ETag or auth scope changes.",[569,622,626],{"className":623,"code":624,"language":625,"meta":574,"style":574},"language-javascript shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","\u002F\u002F Device-side SWR — zero-network hits, works offline, per-request logic.\nasync function swr(request) {\n  const cache = await caches.open('data-v1');\n  const cached = await cache.match(request);\n  const fresh = fetch(request).then((res) => {\n    if (res.ok) cache.put(request, res.clone());\n    return res;\n  });\n  \u002F\u002F trade-off: this runs on the device's main-thread event loop. A large\n  \u002F\u002F synchronous response body cloned\u002Fparsed here can blow the 50ms long-task\n  \u002F\u002F budget and regress INP — do NOT use it for big payloads without chunking,\n  \u002F\u002F and prefer edge SWR when you don't actually need offline.\n  return cached || fresh;\n}\n","javascript",[29,627,628,634,658,688,708,741,761,770,776,782,788,794,800,815],{"__ignoreMap":574},[578,629,630],{"class":264,"line":580},[578,631,633],{"class":632},"sIIH1","\u002F\u002F Device-side SWR — zero-network hits, works offline, per-request logic.\n",[578,635,636,640,643,647,651,655],{"class":264,"line":586},[578,637,639],{"class":638},"sP5qI","async",[578,641,642],{"class":638}," function",[578,644,646],{"class":645},"ssM3C"," swr",[578,648,650],{"class":649},"syybb","(",[578,652,654],{"class":653},"seIZK","request",[578,656,657],{"class":649},") {\n",[578,659,660,663,667,670,673,676,679,681,685],{"class":264,"line":592},[578,661,662],{"class":638},"  const",[578,664,666],{"class":665},"sf6mN"," cache",[578,668,669],{"class":638}," =",[578,671,672],{"class":638}," await",[578,674,675],{"class":649}," caches.",[578,677,678],{"class":645},"open",[578,680,650],{"class":649},[578,682,684],{"class":683},"s-_DF","'data-v1'",[578,686,687],{"class":649},");\n",[578,689,690,692,695,697,699,702,705],{"class":264,"line":598},[578,691,662],{"class":638},[578,693,694],{"class":665}," cached",[578,696,669],{"class":638},[578,698,672],{"class":638},[578,700,701],{"class":649}," cache.",[578,703,704],{"class":645},"match",[578,706,707],{"class":649},"(request);\n",[578,709,710,712,715,717,720,723,726,729,732,735,738],{"class":264,"line":604},[578,711,662],{"class":638},[578,713,714],{"class":665}," fresh",[578,716,669],{"class":638},[578,718,719],{"class":645}," fetch",[578,721,722],{"class":649},"(request).",[578,724,725],{"class":645},"then",[578,727,728],{"class":649},"((",[578,730,731],{"class":653},"res",[578,733,734],{"class":649},") ",[578,736,737],{"class":638},"=>",[578,739,740],{"class":649}," {\n",[578,742,744,747,750,752,755,758],{"class":264,"line":743},6,[578,745,746],{"class":638},"    if",[578,748,749],{"class":649}," (res.ok) cache.",[578,751,507],{"class":645},[578,753,754],{"class":649},"(request, res.",[578,756,757],{"class":645},"clone",[578,759,760],{"class":649},"());\n",[578,762,764,767],{"class":264,"line":763},7,[578,765,766],{"class":638},"    return",[578,768,769],{"class":649}," res;\n",[578,771,773],{"class":264,"line":772},8,[578,774,775],{"class":649},"  });\n",[578,777,779],{"class":264,"line":778},9,[578,780,781],{"class":632},"  \u002F\u002F trade-off: this runs on the device's main-thread event loop. A large\n",[578,783,785],{"class":264,"line":784},10,[578,786,787],{"class":632},"  \u002F\u002F synchronous response body cloned\u002Fparsed here can blow the 50ms long-task\n",[578,789,791],{"class":264,"line":790},11,[578,792,793],{"class":632},"  \u002F\u002F budget and regress INP — do NOT use it for big payloads without chunking,\n",[578,795,797],{"class":264,"line":796},12,[578,798,799],{"class":632},"  \u002F\u002F and prefer edge SWR when you don't actually need offline.\n",[578,801,803,806,809,812],{"class":264,"line":802},13,[578,804,805],{"class":638},"  return",[578,807,808],{"class":649}," cached ",[578,810,811],{"class":638},"||",[578,813,814],{"class":649}," fresh;\n",[578,816,818],{"class":264,"line":817},14,[578,819,820],{"class":649},"}\n",[14,822,823,825,826,829,830,138],{},[61,824,612],{}," repeat-visit LCP drops toward the local-read floor (a Cache API hit is effectively zero-network), and the page paints offline. You accept that you are now shipping, versioning, and debugging code with a site-wide blast radius. For the per-route strategy choice ",[102,827,828],{},"inside"," the worker (cache-first vs SWR in a React app), see the ",[18,831,833],{"href":832},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fswr-vs-cache-first-service-worker-for-react-spas\u002F","SWR vs cache-first Service Worker comparison",[561,835,837],{"id":836},"_3-keep-the-workers-revalidation-off-the-critical-path","3. Keep the worker's revalidation off the critical path",[14,839,840],{},"Defer the write so a large body clone\u002Fparse never lands on the interaction path.",[569,842,844],{"className":623,"code":843,"language":625,"meta":574,"style":574},"\u002F\u002F In the fetch handler: respond immediately, revalidate after.\nevent.respondWith(swr(event.request));\nevent.waitUntil(revalidateInBackground(event.request)); \u002F\u002F heavy put\u002Fparse here\n",[29,845,846,851,867],{"__ignoreMap":574},[578,847,848],{"class":264,"line":580},[578,849,850],{"class":632},"\u002F\u002F In the fetch handler: respond immediately, revalidate after.\n",[578,852,853,856,859,861,864],{"class":264,"line":586},[578,854,855],{"class":649},"event.",[578,857,858],{"class":645},"respondWith",[578,860,650],{"class":649},[578,862,863],{"class":645},"swr",[578,865,866],{"class":649},"(event.request));\n",[578,868,869,871,874,876,879,882],{"class":264,"line":592},[578,870,855],{"class":649},[578,872,873],{"class":645},"waitUntil",[578,875,650],{"class":649},[578,877,878],{"class":645},"revalidateInBackground",[578,880,881],{"class":649},"(event.request)); ",[578,883,884],{"class":632},"\u002F\u002F heavy put\u002Fparse here\n",[14,886,887,889,890,892],{},[61,888,612],{}," the worker's ",[29,891,173],{}," and parse work leaves the critical path, no task exceeds the 50ms long-task budget, and INP stays under 200ms on repeat visits.",[561,894,896],{"id":895},"_4-compose-both-layers-deliberately","4. Compose both layers deliberately",[14,898,899],{},"The strongest setups run edge SWR in front of origin to protect it and warm cold clients, with a worker layered in front of the edge to give returning users offline and instant reads. They are not mutually exclusive — they revalidate at different layers.",[14,901,902,904],{},[61,903,612],{}," cold and first-time visitors get the shared edge object; returning visitors get zero-network local reads and offline resilience; origin load stays flat because both layers absorb it.",[14,906,907,908,911],{},"The most consequential difference, and the one that decides most real cases, is ",[102,909,910],{},"who gets served stale and for how long",". Edge SWR is a shared cache: when the window opens, the very next visitor to that PoP gets the stale object and triggers the single background refresh that then benefits everyone behind that node. The staleness is broad but short-lived and self-healing across the whole audience. Worker SWR is per-device: each browser carries its own copy and its own revalidation cycle, so a user who has not returned in a week is served a week-old object on their next visit regardless of how many other users refreshed in the meantime. For frequently-read shared content, the edge's shared cache converges on freshness far faster; for a returning-after-a-gap individual on a flaky connection, the worker's local copy is the only thing that paints at all. Naming which of those two users you are optimising for usually settles the choice.",[14,913,914,915,917,918,920],{},"There is also an operational asymmetry worth weighing explicitly. An edge header change propagates in seconds and reverts just as fast, and a mistake degrades to \"slightly staler than intended\" — annoying, rarely catastrophic. A Service Worker, once installed, is sticky: a buggy ",[29,916,99],{}," handler can intercept ",[102,919,130],{}," request on the origin and a bad deploy can be hard to claw back, because the broken worker is the very thing that controls whether the fixed worker can be fetched. That is why a worker-based approach demands a tested update-and-skipWaiting path and a rehearsed kill switch before it ships, whereas edge SWR carries almost none of that operational tax.",[47,922,924],{"id":923},"verification","Verification",[14,926,927],{},"Confirm SWR is genuinely active at the layer you chose:",[147,929,930,953,970,983,989],{},[58,931,932,111,935,938,939,941,942,504,945,948,949,952],{},[61,933,934],{},"Edge SWR.",[29,936,937],{},"curl -sI"," the route twice just after expiry. Expected outcome: both return ",[29,940,281],{}," fast, the cache-status header flips from ",[29,943,944],{},"STALE",[29,946,947],{},"REVALIDATING"," to ",[29,950,951],{},"HIT",", and the body updates on the second call — proving the edge revalidated in the background, not on the critical path.",[58,954,955,958,959,961,962,965,966,969],{},[61,956,957],{},"Worker SWR."," In DevTools, Application, Service Workers, reload and watch the Network tab: a hit shows the ",[29,960,78],{}," response ",[61,963,964],{},"plus"," one background request to the same URL. Throttle to ",[61,967,968],{},"Offline"," and confirm the page still paints.",[58,971,972,975,976,978,979,982],{},[61,973,974],{},"INP guard (worker only)."," Record a reload in the Performance panel and confirm the worker's ",[29,977,173],{},"\u002Fparse callback creates no task over 50ms; defer heavy writes with ",[29,980,981],{},"event.waitUntil"," so they leave the critical path.",[58,984,985,988],{},[61,986,987],{},"RUM field check."," Compare TTFB p75 for edge SWR and repeat-visit LCP for worker SWR before and after rollout. Expected outcome: edge SWR holds TTFB ≤ 200ms across PoPs; worker SWR drops repeat-visit LCP toward the local-read floor.",[58,990,991,994,995,997],{},[61,992,993],{},"Rollback drill."," For edge, revert the header and reconfirm; for the worker, ship a no-op ",[29,996,99],{}," passthrough and confirm the site still works — a worker rollback is the riskier of the two, so rehearse it.",[14,999,1000,1001,1005],{},"Choosing the layer deliberately — and composing them where it pays — is what separates a resilient cache from a fragile one. When either layer hands you a stale object you must actively evict, the ",[18,1002,1004],{"href":1003},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F","cache invalidation patterns"," guide covers the purge mechanics.",[47,1007,1009],{"id":1008},"related","Related",[55,1011,1012,1018,1024,1031,1037],{},[58,1013,1014,1017],{},[18,1015,1016],{"href":20},"Stale-while-revalidate implementation"," — the parent guide on SWR across headers, edge, and client.",[58,1019,1020,1023],{},[18,1021,1022],{"href":832},"SWR vs cache-first Service Worker for React SPAs"," — choosing a strategy inside the worker itself.",[58,1025,1026,1030],{},[18,1027,1029],{"href":1028},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fconfiguring-stale-if-error-for-origin-outages\u002F","Configuring stale-if-error for origin outages"," — the resilience directive that pairs with edge SWR.",[58,1032,1033,1036],{},[18,1034,1035],{"href":1003},"Cache invalidation patterns"," — evicting stale objects from either layer after a change.",[58,1038,1039,1041],{},[18,1040,26],{"href":25}," — the broader architecture both layers serve.",[1043,1044,1046],"script",{"type":1045},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"Article\",\n  \"headline\": \"SWR via Cache-Control vs Service Worker revalidation\",\n  \"description\": \"A decision matrix comparing stale-while-revalidate at the CDN edge against stale-while-revalidate implemented inside a Service Worker, covering freshness, offline, INP\u002FLCP, and complexity.\",\n  \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation\u002F\",\n  \"datePublished\": \"2026-06-18\",\n  \"dateModified\": \"2026-07-18\",\n  \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\", \"logo\": { \"@type\": \"ImageObject\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Flogo.svg\" } },\n  \"mainEntityOfPage\": { \"@type\": \"WebPage\", \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation\u002F\" }\n}\n",[1043,1048,1049],{"type":1045},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Choose between Cache-Control SWR at the edge and Service Worker SWR\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"name\": \"Verify edge SWR\", \"text\": \"curl the route twice after expiry and confirm a fast STALE-to-HIT transition with an updated body.\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Verify worker SWR\", \"text\": \"Confirm a (ServiceWorker) hit plus a background request, and that the page paints offline.\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Guard INP\", \"text\": \"Confirm the worker's cache write creates no task over the 50ms long-task budget; defer with waitUntil.\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Check RUM\", \"text\": \"Confirm edge SWR holds TTFB under 200ms and worker SWR lowers repeat-visit LCP.\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Rehearse rollback\", \"text\": \"Revert the header for edge, or ship a passthrough worker, and confirm the site still works.\" }\n  ]\n}\n",[1043,1051,1052],{"type":1045},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Advanced Caching Strategies & CDN Architecture\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Stale-While-Revalidate Implementation\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"SWR via Cache-Control vs Service Worker revalidation\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation\u002F\" }\n  ]\n}\n",[1054,1055,1056],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sIIH1, html code.shiki .sIIH1{--shiki-default:#66707B;--shiki-dark:#66707B;--shiki-light:#66707B}html pre.shiki code .sP5qI, html code.shiki .sP5qI{--shiki-default:#A0111F;--shiki-dark:#A0111F;--shiki-light:#A0111F}html pre.shiki code .ssM3C, html code.shiki .ssM3C{--shiki-default:#622CBC;--shiki-dark:#622CBC;--shiki-light:#622CBC}html pre.shiki code .syybb, html code.shiki .syybb{--shiki-default:#0E1116;--shiki-dark:#0E1116;--shiki-light:#0E1116}html pre.shiki code .seIZK, html code.shiki .seIZK{--shiki-default:#702C00;--shiki-dark:#702C00;--shiki-light:#702C00}html pre.shiki code .sf6mN, html code.shiki .sf6mN{--shiki-default:#023B95;--shiki-dark:#023B95;--shiki-light:#023B95}html pre.shiki code .s-_DF, html code.shiki .s-_DF{--shiki-default:#032563;--shiki-dark:#032563;--shiki-light:#032563}",{"title":574,"searchDepth":586,"depth":586,"links":1058},[1059,1060,1061,1062,1063,1069,1070],{"id":49,"depth":586,"text":50},{"id":141,"depth":586,"text":142},{"id":189,"depth":586,"text":190},{"id":411,"depth":586,"text":412},{"id":555,"depth":586,"text":556,"children":1064},[1065,1066,1067,1068],{"id":563,"depth":592,"text":564},{"id":616,"depth":592,"text":617},{"id":836,"depth":592,"text":837},{"id":895,"depth":592,"text":896},{"id":923,"depth":586,"text":924},{"id":1008,"depth":586,"text":1009},"A decision matrix comparing stale-while-revalidate at the CDN edge against stale-while-revalidate implemented inside a Service Worker.","md",{"slug":1074,"type":1075,"breadcrumb":1076,"datePublished":1084,"dateModified":1085},"swr-cache-control-vs-service-worker-revalidation","article",[1077,1079,1080,1082],{"name":1078,"url":504},"Home",{"name":26,"url":25},{"name":1081,"url":20},"Stale-While-Revalidate Implementation",{"name":5,"url":1083},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation\u002F","2026-06-18","2026-07-18",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation",{"title":1089,"description":1090},"SWR: Cache-Control vs Service Worker","Compare stale-while-revalidate via HTTP Cache-Control at the CDN against SWR in a Service Worker: a matrix on freshness, control, INP\u002FLCP, and complexity.","advanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation\u002Findex","SlUVwcRLq4YpiTShQVz4ZgFSRJO-q33x4LXkLBjcMtA",[1094,1097],{"title":1081,"path":1095,"stem":1096,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation","advanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Findex",{"title":1098,"path":1099,"stem":1100,"children":-1},"Core Web Vitals & Measurement","\u002Fcore-web-vitals-measurement","core-web-vitals-measurement\u002Findex",1784398533901]