[{"data":1,"prerenderedAt":1283},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox":1274},{"id":4,"title":5,"body":6,"description":1256,"extension":1257,"meta":1258,"navigation":399,"path":1269,"seo":1270,"stem":1272,"__hash__":1273},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002Findex.md","Precaching vs Runtime Caching with Workbox",{"type":7,"value":8,"toc":1247},"minimark",[9,14,29,32,37,76,210,214,303,307,317,332,342,353,357,363,549,552,557,772,775,781,927,937,941,944,947,1129,1132,1136,1148,1157,1170,1186,1199,1208,1212,1232,1237,1240,1243],[10,11,13],"h1",{"id":12},"precaching-vs-runtime-caching-what-belongs-in-the-install-manifest","Precaching vs Runtime Caching: What Belongs in the Install Manifest?",[15,16,17,18,23,24,28],"p",{},"This comparison sits under ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002F","Service Worker Caching Strategies"," inside ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Precaching downloads a fixed manifest of files during service-worker install, so those files are guaranteed present afterwards — including offline. Runtime caching stores responses as they are requested, so nothing is downloaded speculatively but nothing is guaranteed either.",[15,30,31],{},"Teams routinely precache the entire build, turning a first visit into a multi-megabyte background download that competes with the page the user is actually reading.",[33,34,36],"h2",{"id":35},"rapid-diagnosis-what-is-your-install-costing","Rapid Diagnosis: What Is Your Install Costing?",[38,39,40,48,54,60],"ul",{},[41,42,43,47],"li",{},[44,45,46],"strong",{},"Measure the install payload."," In Application → Service Workers, trigger an install and watch the Network panel. Anything above a few hundred kilobytes deserves scrutiny.",[41,49,50,53],{},[44,51,52],{},"Check when it runs."," A precache that starts during initial page load competes for bandwidth with the LCP resource. It should be deferred until the page is idle.",[41,55,56,59],{},[44,57,58],{},"Count files versus files used."," If the manifest has 300 entries and a typical session touches 20, you are paying for 280.",[41,61,62,65,66,70,71,75],{},[44,63,64],{},"Look at storage pressure."," ",[67,68,69],"code",{},"navigator.storage.estimate()"," tells you how much you have taken; eviction under pressure removes ",[72,73,74],"em",{},"your whole origin's"," storage, not just the excess.",[77,78,85,86,65,93,65,97,65,101,65,110,65,117,65,122,65,132,65,139,65,144,65,149,65,152,65,156,65,161,65,165,65,168,65,173,65,179,65,183,65,185,65,189,65,192,65,196,65,199,65,205,65],"svg",{"xmlns":79,"viewBox":80,"width":81,"role":82,"ariaLabel":83,"style":84},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 248","100%","img","Comparison of a precache install downloading the whole manifest up front against runtime caching populating gradually as pages are visited, with the guaranteed offline set marked in each","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[87,88],"rect",{"className":89,"x":91,"y":91,"width":81,"height":81,"fill":92},[90],"svg-canvas","0","#ffffff",[94,95,96],"title",{},"Precache install cost versus runtime accumulation",[98,99,100],"desc",{},"Precaching downloads the full manifest during install and guarantees offline availability; runtime caching downloads nothing extra but only holds what has already been requested.",[87,102],{"x":103,"y":103,"width":104,"height":105,"rx":106,"fill":107,"stroke":108,"style":109},"1","758","246","10","none","currentColor","stroke-opacity:0.18",[111,112,116],"text",{"x":113,"y":114,"fill":108,"style":115},"24","32","font-size:16px;font-weight:700","What gets downloaded, and when",[111,118,121],{"x":113,"y":119,"fill":108,"style":120},"76","font-size:11.5px;font-weight:700","Precache",[87,123],{"x":124,"y":125,"width":126,"height":127,"rx":128,"fill":129,"stroke":130,"style":131},"120","58","300","28","4","#ffc300","#b8860b","fill-opacity:0.3",[111,133,138],{"x":134,"y":135,"fill":108,"style":136,"textAnchor":137},"270","77","font-size:11px;font-weight:700;","middle","install: entire manifest, up front",[87,140],{"x":141,"y":125,"width":142,"height":127,"rx":128,"fill":108,"stroke":108,"style":143},"430","70","fill-opacity:0.06;stroke-opacity:0.3",[111,145,148],{"x":146,"y":135,"fill":108,"style":147,"textAnchor":137},"465","font-size:10px;","visit 1",[87,150],{"x":151,"y":125,"width":142,"height":127,"rx":128,"fill":108,"stroke":108,"style":143},"510",[111,153,155],{"x":154,"y":135,"fill":108,"style":147,"textAnchor":137},"545","visit 2",[111,157,160],{"x":158,"y":135,"fill":108,"style":159},"600","font-size:11px;fill-opacity:0.8","offline: guaranteed",[111,162,164],{"x":113,"y":163,"fill":108,"style":120},"146","Runtime",[87,166],{"x":124,"y":167,"width":142,"height":127,"rx":128,"fill":108,"stroke":108,"style":143},"128",[111,169,172],{"x":170,"y":171,"fill":108,"style":147,"textAnchor":137},"155","147","install",[87,174],{"x":175,"y":167,"width":176,"height":127,"rx":128,"fill":177,"stroke":177,"style":178},"200","90","#0466c8","fill-opacity:0.2;stroke-opacity:0.55",[111,180,182],{"x":181,"y":171,"fill":108,"style":147,"textAnchor":137},"245","visit 1 caches",[87,184],{"x":126,"y":167,"width":124,"height":127,"rx":128,"fill":177,"stroke":177,"style":178},[111,186,188],{"x":187,"y":171,"fill":108,"style":147,"textAnchor":137},"360","visit 2 adds more",[87,190],{"x":141,"y":167,"width":191,"height":127,"rx":128,"fill":177,"stroke":177,"style":178},"150",[111,193,195],{"x":194,"y":171,"fill":108,"style":147,"textAnchor":137},"505","grows with real use",[111,197,198],{"x":158,"y":171,"fill":108,"style":159},"offline: whatever is there",[200,201],"line",{"x1":113,"y1":202,"x2":203,"y2":202,"stroke":108,"style":204},"180","736","stroke-opacity:0.2",[111,206,209],{"x":113,"y":207,"fill":108,"style":208},"208","font-size:11.5px;fill-opacity:0.85","The question is never \"which is better\" — it is which files need a guarantee.",[33,211,213],{"id":212},"the-comparison","The Comparison",[215,216,217,233],"table",{},[218,219,220],"thead",{},[221,222,223,227,230],"tr",{},[224,225,226],"th",{},"Axis",[224,228,229],{},"Precaching",[224,231,232],{},"Runtime caching",[234,235,236,248,259,270,281,292],"tbody",{},[221,237,238,242,245],{},[239,240,241],"td",{},"Install cost",[239,243,244],{},"Full manifest, up front",[239,246,247],{},"Zero",[221,249,250,253,256],{},[239,251,252],{},"Offline guarantee",[239,254,255],{},"Yes, for listed files",[239,257,258],{},"Only for what was visited",[221,260,261,264,267],{},[239,262,263],{},"Update mechanism",[239,265,266],{},"Revision hashes in the manifest",[239,268,269],{},"Per-strategy expiry and revalidation",[221,271,272,275,278],{},[239,273,274],{},"Storage growth",[239,276,277],{},"Bounded and known",[239,279,280],{},"Unbounded without an expiration plugin",[221,282,283,286,289],{},[239,284,285],{},"Best for",[239,287,288],{},"App shell, offline fallback, critical routes",[239,290,291],{},"Images, API responses, rarely visited pages",[221,293,294,297,300],{},[239,295,296],{},"Failure mode",[239,298,299],{},"Bloated install, wasted bytes",[239,301,302],{},"Cold cache when it matters",[33,304,306],{"id":305},"root-cause-analysis-how-both-go-wrong","Root Cause Analysis: How Both Go Wrong",[15,308,309,312,313,316],{},[44,310,311],{},"1. Precaching a glob of the whole build."," A default ",[67,314,315],{},"globPatterns"," that matches every emitted asset includes route chunks nobody visits and images for pages that will never be opened.",[15,318,319,322,323,326,327,331],{},[44,320,321],{},"2. Precaching content-hashed assets that the HTTP cache already handles."," Immutable assets served with a year-long ",[67,324,325],{},"Cache-Control"," are already fast on repeat visits — see ",[19,328,330],{"href":329},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002Fsetting-up-immutable-cache-headers-for-hashed-assets\u002F","setting up immutable cache headers",". Precaching them buys offline availability only, so it is worth it only if offline is a requirement.",[15,333,334,337,338,341],{},[44,335,336],{},"3. Runtime caching with no expiration."," A ",[67,339,340],{},"CacheFirst"," image strategy without a maximum entry count grows until the browser evicts the entire origin's storage — including the precache you depended on.",[15,343,344,347,348,352],{},[44,345,346],{},"4. Precaching HTML with the wrong strategy."," A precached HTML document is served from the old service worker until the new one activates, which is exactly the stale-page-after-deploy problem covered in ",[19,349,351],{"href":350},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F","fixing stale HTML after a deploy",".",[33,354,356],{"id":355},"step-by-step-resolution","Step-by-Step Resolution",[15,358,359,362],{},[44,360,361],{},"1. Precache only the guaranteed set."," The app shell, the offline fallback page, and the assets those two need — nothing else.",[364,365,370],"pre",{"className":366,"code":367,"language":368,"meta":369,"style":369},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","import { precacheAndRoute } from 'workbox-precaching';\n\n\u002F\u002F Injected by the build, but SCOPED: shell and fallback only.\nprecacheAndRoute([\n  { url: '\u002Foffline.html', revision: '8f2c1a' },\n  { url: '\u002Fapp-shell.html', revision: '8f2c1a' },\n  ...self.__WB_MANIFEST.filter((e) => \u002F\\\u002F(app|shell)\\.[a-f0-9]+\\.(js|css)$\u002F.test(e.url))\n]);\n\u002F\u002F trade-off: a narrow precache means a user who goes offline on an unvisited\n\u002F\u002F route gets the fallback page rather than the real one. That is the correct\n\u002F\u002F default — offering every route offline costs every user the download.\n","javascript","",[67,371,372,394,401,408,418,436,450,525,531,537,543],{"__ignoreMap":369},[373,374,376,380,384,387,391],"span",{"class":200,"line":375},1,[373,377,379],{"class":378},"sPARh","import",[373,381,383],{"class":382},"saISM"," { precacheAndRoute } ",[373,385,386],{"class":378},"from",[373,388,390],{"class":389},"sZ8jY"," 'workbox-precaching'",[373,392,393],{"class":382},";\n",[373,395,397],{"class":200,"line":396},2,[373,398,400],{"emptyLinePlaceholder":399},true,"\n",[373,402,404],{"class":200,"line":403},3,[373,405,407],{"class":406},"sjfSM","\u002F\u002F Injected by the build, but SCOPED: shell and fallback only.\n",[373,409,411,415],{"class":200,"line":410},4,[373,412,414],{"class":413},"smZ65","precacheAndRoute",[373,416,417],{"class":382},"([\n",[373,419,421,424,427,430,433],{"class":200,"line":420},5,[373,422,423],{"class":382},"  { url: ",[373,425,426],{"class":389},"'\u002Foffline.html'",[373,428,429],{"class":382},", revision: ",[373,431,432],{"class":389},"'8f2c1a'",[373,434,435],{"class":382}," },\n",[373,437,439,441,444,446,448],{"class":200,"line":438},6,[373,440,423],{"class":382},[373,442,443],{"class":389},"'\u002Fapp-shell.html'",[373,445,429],{"class":382},[373,447,432],{"class":389},[373,449,435],{"class":382},[373,451,453,456,459,462,465,469,472,475,478,482,485,488,491,494,498,501,503,506,508,511,514,517,519,522],{"class":200,"line":452},7,[373,454,455],{"class":378},"  ...",[373,457,458],{"class":382},"self.__WB_MANIFEST.",[373,460,461],{"class":413},"filter",[373,463,464],{"class":382},"((",[373,466,468],{"class":467},"sQw3B","e",[373,470,471],{"class":382},") ",[373,473,474],{"class":378},"=>",[373,476,477],{"class":389}," \u002F",[373,479,481],{"class":480},"sn2CQ","\\\u002F",[373,483,484],{"class":389},"(app",[373,486,487],{"class":378},"|",[373,489,490],{"class":389},"shell)",[373,492,493],{"class":480},"\\.",[373,495,497],{"class":496},"sPXB4","[a-f0-9]",[373,499,500],{"class":378},"+",[373,502,493],{"class":480},[373,504,505],{"class":389},"(js",[373,507,487],{"class":378},[373,509,510],{"class":389},"css)",[373,512,513],{"class":378},"$",[373,515,516],{"class":389},"\u002F",[373,518,352],{"class":382},[373,520,521],{"class":413},"test",[373,523,524],{"class":382},"(e.url))\n",[373,526,528],{"class":200,"line":527},8,[373,529,530],{"class":382},"]);\n",[373,532,534],{"class":200,"line":533},9,[373,535,536],{"class":406},"\u002F\u002F trade-off: a narrow precache means a user who goes offline on an unvisited\n",[373,538,540],{"class":200,"line":539},10,[373,541,542],{"class":406},"\u002F\u002F route gets the fallback page rather than the real one. That is the correct\n",[373,544,546],{"class":200,"line":545},11,[373,547,548],{"class":406},"\u002F\u002F default — offering every route offline costs every user the download.\n",[15,550,551],{},"Expected outcome: install payload drops from megabytes to tens of kilobytes.",[15,553,554],{},[44,555,556],{},"2. Runtime-cache everything else with explicit limits.",[364,558,560],{"className":366,"code":559,"language":368,"meta":369,"style":369},"import { registerRoute } from 'workbox-routing';\nimport { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';\nimport { ExpirationPlugin } from 'workbox-expiration';\n\nregisterRoute(({ request }) => request.destination === 'image',\n  new CacheFirst({\n    cacheName: 'images',\n    plugins: [new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 86400,\n                                     purgeOnQuotaError: true })]\n  }));\n\nregisterRoute(({ url }) => url.pathname.startsWith('\u002Fapi\u002F'),\n  new StaleWhileRevalidate({ cacheName: 'api-v1' }));\n\u002F\u002F trade-off: purgeOnQuotaError lets the browser drop this cache under pressure\n\u002F\u002F instead of evicting everything. It means images can vanish silently — which\n\u002F\u002F is right for images and wrong for anything the app depends on.\n",[67,561,562,576,590,604,608,636,647,657,688,699,704,708,737,754,760,766],{"__ignoreMap":369},[373,563,564,566,569,571,574],{"class":200,"line":375},[373,565,379],{"class":378},[373,567,568],{"class":382}," { registerRoute } ",[373,570,386],{"class":378},[373,572,573],{"class":389}," 'workbox-routing'",[373,575,393],{"class":382},[373,577,578,580,583,585,588],{"class":200,"line":396},[373,579,379],{"class":378},[373,581,582],{"class":382}," { CacheFirst, StaleWhileRevalidate } ",[373,584,386],{"class":378},[373,586,587],{"class":389}," 'workbox-strategies'",[373,589,393],{"class":382},[373,591,592,594,597,599,602],{"class":200,"line":403},[373,593,379],{"class":378},[373,595,596],{"class":382}," { ExpirationPlugin } ",[373,598,386],{"class":378},[373,600,601],{"class":389}," 'workbox-expiration'",[373,603,393],{"class":382},[373,605,606],{"class":200,"line":410},[373,607,400],{"emptyLinePlaceholder":399},[373,609,610,613,616,619,622,624,627,630,633],{"class":200,"line":420},[373,611,612],{"class":413},"registerRoute",[373,614,615],{"class":382},"(({ ",[373,617,618],{"class":467},"request",[373,620,621],{"class":382}," }) ",[373,623,474],{"class":378},[373,625,626],{"class":382}," request.destination ",[373,628,629],{"class":378},"===",[373,631,632],{"class":389}," 'image'",[373,634,635],{"class":382},",\n",[373,637,638,641,644],{"class":200,"line":438},[373,639,640],{"class":378},"  new",[373,642,643],{"class":413}," CacheFirst",[373,645,646],{"class":382},"({\n",[373,648,649,652,655],{"class":200,"line":452},[373,650,651],{"class":382},"    cacheName: ",[373,653,654],{"class":389},"'images'",[373,656,635],{"class":382},[373,658,659,662,665,668,671,674,677,680,683,686],{"class":200,"line":527},[373,660,661],{"class":382},"    plugins: [",[373,663,664],{"class":378},"new",[373,666,667],{"class":413}," ExpirationPlugin",[373,669,670],{"class":382},"({ maxEntries: ",[373,672,673],{"class":496},"60",[373,675,676],{"class":382},", maxAgeSeconds: ",[373,678,679],{"class":496},"30",[373,681,682],{"class":378}," *",[373,684,685],{"class":496}," 86400",[373,687,635],{"class":382},[373,689,690,693,696],{"class":200,"line":533},[373,691,692],{"class":382},"                                     purgeOnQuotaError: ",[373,694,695],{"class":496},"true",[373,697,698],{"class":382}," })]\n",[373,700,701],{"class":200,"line":539},[373,702,703],{"class":382},"  }));\n",[373,705,706],{"class":200,"line":545},[373,707,400],{"emptyLinePlaceholder":399},[373,709,711,713,715,718,720,722,725,728,731,734],{"class":200,"line":710},12,[373,712,612],{"class":413},[373,714,615],{"class":382},[373,716,717],{"class":467},"url",[373,719,621],{"class":382},[373,721,474],{"class":378},[373,723,724],{"class":382}," url.pathname.",[373,726,727],{"class":413},"startsWith",[373,729,730],{"class":382},"(",[373,732,733],{"class":389},"'\u002Fapi\u002F'",[373,735,736],{"class":382},"),\n",[373,738,740,742,745,748,751],{"class":200,"line":739},13,[373,741,640],{"class":378},[373,743,744],{"class":413}," StaleWhileRevalidate",[373,746,747],{"class":382},"({ cacheName: ",[373,749,750],{"class":389},"'api-v1'",[373,752,753],{"class":382}," }));\n",[373,755,757],{"class":200,"line":756},14,[373,758,759],{"class":406},"\u002F\u002F trade-off: purgeOnQuotaError lets the browser drop this cache under pressure\n",[373,761,763],{"class":200,"line":762},15,[373,764,765],{"class":406},"\u002F\u002F instead of evicting everything. It means images can vanish silently — which\n",[373,767,769],{"class":200,"line":768},16,[373,770,771],{"class":406},"\u002F\u002F is right for images and wrong for anything the app depends on.\n",[15,773,774],{},"Expected outcome: caches stay bounded and eviction, when it happens, removes the least important data first.",[15,776,777,780],{},[44,778,779],{},"3. Defer the install work until the page is idle."," A precache that competes with the first paint costs LCP for a benefit that only materialises on the next visit.",[364,782,784],{"className":366,"code":783,"language":368,"meta":369,"style":369},"\u002F\u002F In the page, not the worker: register after load, when the thread is free.\naddEventListener('load', () => {\n  if (!('serviceWorker' in navigator)) return;\n  const start = () => navigator.serviceWorker.register('\u002Fsw.js');\n  'requestIdleCallback' in window ? requestIdleCallback(start, { timeout: 4000 })\n                                  : setTimeout(start, 2000);\n});\n\u002F\u002F trade-off: deferring means a very short first visit may end before the worker\n\u002F\u002F installs, so the second visit pays the install instead. That is strictly\n\u002F\u002F better than slowing the first one.\n",[67,785,786,791,809,836,866,891,907,912,917,922],{"__ignoreMap":369},[373,787,788],{"class":200,"line":375},[373,789,790],{"class":406},"\u002F\u002F In the page, not the worker: register after load, when the thread is free.\n",[373,792,793,796,798,801,804,806],{"class":200,"line":396},[373,794,795],{"class":413},"addEventListener",[373,797,730],{"class":382},[373,799,800],{"class":389},"'load'",[373,802,803],{"class":382},", () ",[373,805,474],{"class":378},[373,807,808],{"class":382}," {\n",[373,810,811,814,817,820,822,825,828,831,834],{"class":200,"line":403},[373,812,813],{"class":378},"  if",[373,815,816],{"class":382}," (",[373,818,819],{"class":378},"!",[373,821,730],{"class":382},[373,823,824],{"class":389},"'serviceWorker'",[373,826,827],{"class":378}," in",[373,829,830],{"class":382}," navigator)) ",[373,832,833],{"class":378},"return",[373,835,393],{"class":382},[373,837,838,841,844,847,850,852,855,858,860,863],{"class":200,"line":410},[373,839,840],{"class":378},"  const",[373,842,843],{"class":413}," start",[373,845,846],{"class":378}," =",[373,848,849],{"class":382}," () ",[373,851,474],{"class":378},[373,853,854],{"class":382}," navigator.serviceWorker.",[373,856,857],{"class":413},"register",[373,859,730],{"class":382},[373,861,862],{"class":389},"'\u002Fsw.js'",[373,864,865],{"class":382},");\n",[373,867,868,871,873,876,879,882,885,888],{"class":200,"line":420},[373,869,870],{"class":389},"  'requestIdleCallback'",[373,872,827],{"class":378},[373,874,875],{"class":382}," window ",[373,877,878],{"class":378},"?",[373,880,881],{"class":413}," requestIdleCallback",[373,883,884],{"class":382},"(start, { timeout: ",[373,886,887],{"class":496},"4000",[373,889,890],{"class":382}," })\n",[373,892,893,896,899,902,905],{"class":200,"line":438},[373,894,895],{"class":378},"                                  :",[373,897,898],{"class":413}," setTimeout",[373,900,901],{"class":382},"(start, ",[373,903,904],{"class":496},"2000",[373,906,865],{"class":382},[373,908,909],{"class":200,"line":452},[373,910,911],{"class":382},"});\n",[373,913,914],{"class":200,"line":527},[373,915,916],{"class":406},"\u002F\u002F trade-off: deferring means a very short first visit may end before the worker\n",[373,918,919],{"class":200,"line":533},[373,920,921],{"class":406},"\u002F\u002F installs, so the second visit pays the install instead. That is strictly\n",[373,923,924],{"class":200,"line":539},[373,925,926],{"class":406},"\u002F\u002F better than slowing the first one.\n",[15,928,929,932,933,936],{},[44,930,931],{},"4. Version the runtime caches deliberately."," Include a version in each ",[67,934,935],{},"cacheName"," and delete old ones on activate, so a schema change cannot leave incompatible responses behind.",[33,938,940],{"id":939},"verification","Verification",[15,942,943],{},"Confirm the install cost first: clear storage, load the page, and check total bytes transferred during install in the Network panel. Then check the guarantee you paid for — go offline and load the shell and the fallback page.",[15,945,946],{},"Watch storage over a realistic session:",[364,948,950],{"className":366,"code":949,"language":368,"meta":369,"style":369},"const { usage, quota } = await navigator.storage.estimate();\nconsole.log(`${(usage \u002F 1048576).toFixed(1)}MB of ${(quota \u002F 1048576).toFixed(0)}MB`);\nfor (const name of await caches.keys()) {\n  console.log(name, (await (await caches.open(name)).keys()).length, 'entries');\n}\n\u002F\u002F Expect bounded counts per cache. A count that keeps climbing across sessions\n\u002F\u002F means an expiration plugin is missing on that route.\n",[67,951,952,987,1048,1074,1114,1119,1124],{"__ignoreMap":369},[373,953,954,957,960,963,966,969,972,975,978,981,984],{"class":200,"line":375},[373,955,956],{"class":378},"const",[373,958,959],{"class":382}," { ",[373,961,962],{"class":496},"usage",[373,964,965],{"class":382},", ",[373,967,968],{"class":496},"quota",[373,970,971],{"class":382}," } ",[373,973,974],{"class":378},"=",[373,976,977],{"class":378}," await",[373,979,980],{"class":382}," navigator.storage.",[373,982,983],{"class":413},"estimate",[373,985,986],{"class":382},"();\n",[373,988,989,992,995,997,1000,1002,1004,1006,1009,1012,1015,1017,1019,1022,1025,1027,1029,1031,1033,1035,1037,1039,1041,1043,1046],{"class":200,"line":396},[373,990,991],{"class":382},"console.",[373,993,994],{"class":413},"log",[373,996,730],{"class":382},[373,998,999],{"class":389},"`${",[373,1001,730],{"class":389},[373,1003,962],{"class":382},[373,1005,477],{"class":378},[373,1007,1008],{"class":496}," 1048576",[373,1010,1011],{"class":389},").",[373,1013,1014],{"class":413},"toFixed",[373,1016,730],{"class":389},[373,1018,103],{"class":496},[373,1020,1021],{"class":389},")",[373,1023,1024],{"class":389},"}MB of ${",[373,1026,730],{"class":389},[373,1028,968],{"class":382},[373,1030,477],{"class":378},[373,1032,1008],{"class":496},[373,1034,1011],{"class":389},[373,1036,1014],{"class":413},[373,1038,730],{"class":389},[373,1040,91],{"class":496},[373,1042,1021],{"class":389},[373,1044,1045],{"class":389},"}MB`",[373,1047,865],{"class":382},[373,1049,1050,1053,1055,1057,1060,1063,1065,1068,1071],{"class":200,"line":403},[373,1051,1052],{"class":378},"for",[373,1054,816],{"class":382},[373,1056,956],{"class":378},[373,1058,1059],{"class":496}," name",[373,1061,1062],{"class":378}," of",[373,1064,977],{"class":378},[373,1066,1067],{"class":382}," caches.",[373,1069,1070],{"class":413},"keys",[373,1072,1073],{"class":382},"()) {\n",[373,1075,1076,1079,1081,1084,1087,1089,1091,1093,1096,1099,1101,1104,1107,1109,1112],{"class":200,"line":410},[373,1077,1078],{"class":382},"  console.",[373,1080,994],{"class":413},[373,1082,1083],{"class":382},"(name, (",[373,1085,1086],{"class":378},"await",[373,1088,816],{"class":382},[373,1090,1086],{"class":378},[373,1092,1067],{"class":382},[373,1094,1095],{"class":413},"open",[373,1097,1098],{"class":382},"(name)).",[373,1100,1070],{"class":413},[373,1102,1103],{"class":382},"()).",[373,1105,1106],{"class":496},"length",[373,1108,965],{"class":382},[373,1110,1111],{"class":389},"'entries'",[373,1113,865],{"class":382},[373,1115,1116],{"class":200,"line":420},[373,1117,1118],{"class":382},"}\n",[373,1120,1121],{"class":200,"line":438},[373,1122,1123],{"class":406},"\u002F\u002F Expect bounded counts per cache. A count that keeps climbing across sessions\n",[373,1125,1126],{"class":200,"line":452},[373,1127,1128],{"class":406},"\u002F\u002F means an expiration plugin is missing on that route.\n",[15,1130,1131],{},"Finally, verify the update path: deploy a change, reload twice, and confirm the new assets are served. A precache that never updates is worse than no precache, because it pins users to an old build.",[33,1133,1135],{"id":1134},"faq","FAQ",[1137,1138,1141,1145],"details",{"className":1139},[1140],"faq-item",[1142,1143,1144],"summary",{},"Should I precache my JavaScript bundles?",[15,1146,1147],{},"Only the shell's. Content-hashed bundles are already served from the HTTP cache with a long TTL on repeat visits, so precaching adds offline availability and nothing else. If offline use of a route is not a product requirement, runtime-cache those chunks on first use instead and keep the install cheap.",[1137,1149,1151,1154],{"className":1150},[1140],[1142,1152,1153],{},"How large is too large for a precache manifest?",[15,1155,1156],{},"Judge it by download time on a slow connection rather than by a byte count. If the install takes more than a few seconds on a throttled mobile profile, it is competing with real page activity and should be trimmed. In practice, most sites should be well under a megabyte; anything approaching several megabytes almost always includes route chunks that could be runtime-cached.",[1137,1158,1160,1163],{"className":1159},[1140],[1142,1161,1162],{},"What happens when storage quota is exceeded?",[15,1164,1165,1166,1169],{},"The browser evicts storage for the entire origin, not just the cache that overflowed — including IndexedDB and your precache. That is why ",[67,1167,1168],{},"purgeOnQuotaError"," matters: it marks a specific cache as safe to drop first. Track usage against quota and treat sustained growth as a bug, because the failure mode is losing everything at once, usually on the devices with the least space.",[1137,1171,1173,1176],{"className":1172},[1140],[1142,1174,1175],{},"Does a service worker help performance at all if I have good HTTP caching?",[15,1177,1178,1179,1182,1183,1185],{},"For repeat visits on a warm cache, barely — the HTTP cache already serves those bytes without a network request. The service worker earns its place in three other situations: genuine offline support, control over ",[72,1180,1181],{},"stale"," responses so you can serve instantly and revalidate behind the request, and resilience when the network is present but unusable. If none of those is a requirement, correct ",[67,1184,325],{}," headers deliver most of the benefit with none of the update complexity.",[1137,1187,1189,1192],{"className":1188},[1140],[1142,1190,1191],{},"How should I handle API responses in a service worker?",[15,1193,1194,1195,1198],{},"Cache them under a versioned name with a strategy that matches the data's tolerance for staleness — revalidate-on-use for feeds and listings, network-first for anything transactional, and no caching at all for authenticated or personalised responses unless you can key them per user. The mistake to avoid is a single blanket strategy for ",[67,1196,1197],{},"\u002Fapi\u002F",", which inevitably serves one endpoint's stale data where it is not acceptable.",[1137,1200,1202,1205],{"className":1201},[1140],[1142,1203,1204],{},"What happens to the caches when a user clears site data?",[15,1206,1207],{},"Everything goes: the precache, the runtime caches, IndexedDB and the registration itself. The next visit behaves exactly like a first visit, including the install cost — which is another reason to keep that cost small. Design for this rather than against it: a site whose first visit is fast does not need the cache to be fast, while one that depends on a warm cache degrades badly for anyone who clears storage or arrives in a private window.",[33,1209,1211],{"id":1210},"related","Related",[38,1213,1214,1219,1226],{},[41,1215,1216,1218],{},[19,1217,22],{"href":21}," — the strategy vocabulary these decisions draw on.",[41,1220,1221,1225],{},[19,1222,1224],{"href":1223},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fdebugging-service-worker-cache-misses-in-production\u002F","Debugging Service Worker cache misses in production"," — when the cache exists but is not being used.",[41,1227,1228,1231],{},[19,1229,1230],{"href":350},"Fixing stale HTML after a deploy"," — the update path for precached documents.",[1233,1234,1236],"script",{"type":1235},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Split assets between precaching and runtime caching\",\n  \"description\": \"Precache only the guaranteed offline set, runtime-cache the rest with explicit expiration, defer service-worker install until idle, and version runtime caches so updates are safe.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Narrow the precache\", \"text\": \"Precache the app shell, offline fallback and the assets they require, and nothing else.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Bound the runtime caches\", \"text\": \"Register runtime strategies with maximum entry counts, ages and quota-error purging.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Defer the install\", \"text\": \"Register the service worker after load or on idle so precaching does not compete with the first paint.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Version and verify\", \"text\": \"Version each cache name, clean up on activate, and confirm the update path serves new assets after a deploy.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F#verification\" }\n  ]\n}\n",[1233,1238,1239],{"type":1235},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Precaching vs Runtime Caching: What Belongs in the Install Manifest?\",\n  \"description\": \"Decide which assets need an offline guarantee and which should be cached on demand, without paying a multi-megabyte install on every first visit.\",\n  \"datePublished\": \"2026-08-01\",\n  \"dateModified\": \"2026-08-01\",\n  \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"mainEntityOfPage\": { \"@type\": \"WebPage\", \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F\" }\n}\n",[1233,1241,1242],{"type":1235},"\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\": \"Service Worker Caching Strategies\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Precaching vs Runtime Caching with Workbox\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F\" }\n  ]\n}\n",[1244,1245,1246],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sn2CQ, html code.shiki .sn2CQ{--shiki-default:#024C1A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold;--shiki-light:#024C1A;--shiki-light-font-weight:bold}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}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);}",{"title":369,"searchDepth":396,"depth":396,"links":1248},[1249,1250,1251,1252,1253,1254,1255],{"id":35,"depth":396,"text":36},{"id":212,"depth":396,"text":213},{"id":305,"depth":396,"text":306},{"id":355,"depth":396,"text":356},{"id":939,"depth":396,"text":940},{"id":1134,"depth":396,"text":1135},{"id":1210,"depth":396,"text":1211},"Which assets belong in the install manifest and which should be cached on demand — decided by guarantee requirements, not by convenience.","md",{"slug":1259,"type":1260,"breadcrumb":1261,"datePublished":1268,"dateModified":1268},"precaching-vs-runtime-caching-with-workbox","article",[1262,1264,1265,1266],{"name":1263,"url":516},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1267},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F","2026-08-01","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox",{"title":5,"description":1271},"Precache guarantees offline availability at install cost; runtime caching stays lean but has no guarantees. Compare install weight, update behaviour and storage pressure.","advanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002Findex","_Czur91U55jZbsgTBHgJUpXI6EigqLaMXKke5g_StcI",[1275,1279],{"title":1276,"path":1277,"stem":1278,"children":-1},"Debugging Service Worker Cache Misses in Production","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fdebugging-service-worker-cache-misses-in-production","advanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fdebugging-service-worker-cache-misses-in-production\u002Findex",{"title":1280,"path":1281,"stem":1282,"children":-1},"SWR vs cache-first Service Worker for React SPAs","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fswr-vs-cache-first-service-worker-for-react-spas","advanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fswr-vs-cache-first-service-worker-for-react-spas\u002Findex",1785622407135]