[{"data":1,"prerenderedAt":1200},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy":1191},{"id":4,"title":5,"body":6,"description":1170,"extension":1171,"meta":1172,"navigation":1184,"path":1185,"seo":1186,"stem":1189,"__hash__":1190},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy\u002Findex.md","Handling Chunk Load Errors After a Deploy",{"type":7,"value":8,"toc":1155},"minimark",[9,14,38,41,179,184,235,239,245,254,260,266,331,335,340,343,412,415,419,425,507,510,514,517,742,745,749,768,900,1007,1011,1014,1018,1031,1035,1065,1069,1086,1099,1113,1117,1140,1145,1148,1151],[10,11,13],"h1",{"id":12},"how-to-handle-chunk-load-errors-after-a-deploy","How to Handle Chunk Load Errors After a Deploy",[15,16,17,18,23,24,28,29,33,34,37],"p",{},"This guide covers an operational side effect of ",[19,20,22],"a",{"href":21},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002F","Dynamic Imports and Route-Based Splitting",", within ",[19,25,27],{"href":26},"\u002Fjavascript-bundle-optimization-code-splitting\u002F","JavaScript Bundle Optimization & Code Splitting",". Code splitting means the application loads parts of itself later — after the page has been open for minutes or hours. Content-hashed chunk filenames mean every deploy changes the names of changed chunks. Put the two together and a user with a tab open from before the deploy clicks a link, the old runtime requests ",[30,31,32],"code",{},"ProductPage-3f9a.js",", and the server returns a 404 (or worse, the HTML of the new index page with a 200 status). The import fails with ",[30,35,36],{},"ChunkLoadError"," or \"Failed to fetch dynamically imported module\", and the navigation breaks.",[15,39,40],{},"This is not a rare edge case. Sites that deploy several times a day see these errors constantly in error monitoring, often dismissed as noise. For users, it is a click that does nothing — the worst possible interaction, and one INP never records because no paint follows.",[15,42,43],{},[44,45,51,52,51,59,51,63,51,66,51,84,51,92,51,98,51,105,51,112,51,115,51,119,51,122,51,126,51,135,51,138,51,141,51,147,51,153,51,157,51,162,51,167,51,171,51,176,51],"svg",{"viewBox":46,"width":47,"role":48,"ariaLabel":49,"style":50},"0 0 760 290","100%","img","Sequence showing a tab loaded before a deploy requesting a lazy chunk that the deploy removed, receiving a 404 and failing.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[53,54],"rect",{"className":55,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],"svg-canvas","0","#ffffff",[60,61,62],"title",{},"How a deploy breaks an open tab",[64,65,49],"desc",{},[67,68,69],"defs",{},[70,71,78],"marker",{"id":72,"viewBox":73,"refX":74,"refY":75,"markerWidth":76,"markerHeight":76,"orient":77},"fa76292a63","0 0 10 10","9","5","7","auto-start-reverse",[79,80],"path",{"d":81,"fill":82,"style":83},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[53,85],{"x":86,"y":86,"width":87,"height":88,"rx":89,"fill":90,"stroke":82,"style":91},"1","758","288","10","none","stroke-opacity:0.18",[93,94,62],"text",{"x":95,"y":96,"fill":82,"style":97},"28.0","34.0","font-size:16px;font-weight:700",[53,99],{"x":100,"y":101,"width":102,"height":96,"rx":103,"fill":82,"stroke":82,"style":104},"70.3","56.0","150.0","6","fill-opacity:0.06;stroke-opacity:0.4",[93,106,111],{"x":107,"y":108,"fill":82,"style":109,"textAnchor":110},"145.3","78.0","font-size:13px;font-weight:700","middle","Old tab",[53,113],{"x":114,"y":101,"width":102,"height":96,"rx":103,"fill":82,"stroke":82,"style":104},"305.0",[93,116,118],{"x":117,"y":108,"fill":82,"style":109,"textAnchor":110},"380.0","CDN",[53,120],{"x":121,"y":101,"width":102,"height":96,"rx":103,"fill":82,"stroke":82,"style":104},"539.7",[93,123,125],{"x":124,"y":108,"fill":82,"style":109,"textAnchor":110},"614.7","Origin",[127,128],"line",{"x1":107,"y1":129,"x2":107,"y2":130,"stroke":82,"strokeWidth":131,"strokeDashArray":132,"style":134},"90.0","268.0","1.5",[133,133],"4","stroke-opacity:0.35",[127,136],{"x1":117,"y1":129,"x2":117,"y2":130,"stroke":82,"strokeWidth":131,"strokeDashArray":137,"style":134},[133,133],[127,139],{"x1":124,"y1":129,"x2":124,"y2":130,"stroke":82,"strokeWidth":131,"strokeDashArray":140,"style":134},[133,133],[127,142],{"x1":107,"y1":143,"x2":144,"y2":143,"stroke":82,"strokeWidth":131,"style":145,"markerEnd":146},"124.0","378.0","stroke-opacity:0.6","url(#fa76292a63)",[93,148,152],{"x":149,"y":150,"fill":82,"style":151,"textAnchor":110},"262.7","117.0","font-size:12px","GET Product-3f9a.js",[127,154],{"x1":117,"y1":155,"x2":156,"y2":155,"stroke":82,"strokeWidth":131,"style":145,"markerEnd":146},"164.0","612.7",[93,158,161],{"x":159,"y":160,"fill":82,"style":151,"textAnchor":110},"497.3","157.0","cache miss",[127,163],{"x1":124,"y1":164,"x2":165,"y2":164,"stroke":82,"strokeWidth":131,"strokeDashArray":166,"style":145,"markerEnd":146},"204.0","382.0",[75,133],[93,168,170],{"x":159,"y":169,"fill":82,"style":151,"textAnchor":110},"197.0","404 (deleted on deploy)",[127,172],{"x1":117,"y1":173,"x2":174,"y2":173,"stroke":82,"strokeWidth":131,"strokeDashArray":175,"style":145,"markerEnd":146},"244.0","147.3",[75,133],[93,177,36],{"x":149,"y":178,"fill":82,"style":151,"textAnchor":110},"237.0",[180,181,183],"h2",{"id":182},"rapid-diagnosis","Rapid Diagnosis",[185,186,187,209,223,229],"ul",{},[188,189,190,194,195,197,198,197,201,204,205,208],"li",{},[191,192,193],"strong",{},"Search error monitoring"," for ",[30,196,36],{},", ",[30,199,200],{},"Loading chunk",[30,202,203],{},"Failed to fetch dynamically imported module"," and ",[30,206,207],{},"Importing a module script failed",". Plot them against deploy times.",[188,210,211,214,215,218,219,222],{},[191,212,213],{},"Check what the server returns for a missing chunk."," If a request for a nonexistent ",[30,216,217],{},".js"," returns your SPA's ",[30,220,221],{},"index.html"," with status 200, the browser reports a MIME type error instead — and caches may store the HTML under the JS URL.",[188,224,225,228],{},[191,226,227],{},"Check asset retention."," Does your deploy delete the previous build's assets, or upload to a fresh bucket?",[188,230,231,234],{},[191,232,233],{},"Check service worker behaviour."," A service worker serving an old app shell can extend the window during which old chunk names are requested.",[180,236,238],{"id":237},"root-cause-analysis","Root Cause Analysis",[15,240,241,244],{},[191,242,243],{},"1. Old assets deleted on deploy."," Atomic deploys that replace the whole asset directory remove files old tabs still reference.",[15,246,247,250,251,253],{},[191,248,249],{},"2. SPA fallback rewrites."," Hosting rules that rewrite every unknown path to ",[30,252,221],{}," turn a clean 404 into a confusing MIME error and can poison caches.",[15,255,256,259],{},[191,257,258],{},"3. No retry or recovery in the app."," A failed import propagates as an unhandled error; the router aborts navigation and the user is stuck.",[15,261,262,265],{},[191,263,264],{},"4. Long-lived tabs."," Dashboards and SPAs are left open for days; any deploy in that window can break them.",[15,267,268],{},[44,269,51,272,51,275,51,278,51,280,51,283,51,285,51,292,51,301,51,306,51,310,51,316,51,319,51,323,51,327,51],{"viewBox":270,"width":47,"role":48,"ariaLabel":271,"style":50},"0 0 760 163","Bar chart of chunk load errors per ten thousand sessions in the hours after a deploy, under three mitigation levels.",[53,273],{"className":274,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],[60,276,277],{},"Chunk load errors per 10k sessions after a deploy",[64,279,271],{},[53,281],{"x":86,"y":86,"width":87,"height":282,"rx":89,"fill":90,"stroke":82,"style":91},"161",[93,284,277],{"x":95,"y":96,"fill":82,"style":97},[93,286,291],{"x":287,"y":288,"fill":82,"style":289,"textAnchor":290},"210.0","70.0","font-size:13px","end","Old assets deleted, no recovery",[53,293],{"x":294,"y":101,"width":295,"height":296,"rx":297,"fill":298,"stroke":299,"style":300},"222.0","446.0","19","3","#ffc300","#b8860b","fill-opacity:0.7;stroke-opacity:0.9",[93,302,305],{"x":303,"y":288,"fill":82,"style":304},"674.0","font-size:12px;font-weight:600","41",[93,307,309],{"x":287,"y":308,"fill":82,"style":289,"textAnchor":290},"101.0","Old assets kept 7 days",[53,311],{"x":294,"y":312,"width":313,"height":296,"rx":297,"fill":314,"stroke":314,"style":315},"87.0","32.6","#0466c8","fill-opacity:0.55;stroke-opacity:0.9",[93,317,297],{"x":318,"y":308,"fill":82,"style":304},"260.6",[93,320,322],{"x":287,"y":321,"fill":82,"style":289,"textAnchor":290},"132.0","Kept + reload-on-failure",[53,324],{"x":294,"y":325,"width":326,"height":296,"rx":297,"fill":314,"stroke":314,"style":315},"118.0","4.4",[93,328,330],{"x":329,"y":321,"fill":82,"style":304},"232.3","0.4",[180,332,334],{"id":333},"step-by-step-resolution","Step-by-Step Resolution",[336,337,339],"h3",{"id":338},"_1-keep-previous-builds-assets-available","1. Keep previous builds' assets available",[15,341,342],{},"Deploy hashed assets additively: upload new files, never delete recent old ones. Expire assets older than your longest realistic tab lifetime (often 7–30 days) with a lifecycle rule.",[344,345,350],"pre",{"className":346,"code":347,"language":348,"meta":349,"style":349},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# Upload only new hashed files; keep existing ones (no --delete).\naws s3 sync dist\u002Fassets s3:\u002F\u002Fmy-site-assets\u002Fassets \\\n  --cache-control \"public, max-age=31536000, immutable\"\n# HTML is uploaded separately with a short TTL so new visitors get the new build.\n# trade-off: retaining old assets costs storage and keeps old code reachable.\n# Expire them after a fixed window with a bucket lifecycle rule rather than never.\n","bash","",[30,351,352,360,384,394,400,406],{"__ignoreMap":349},[353,354,356],"span",{"class":127,"line":355},1,[353,357,359],{"class":358},"sjfSM","# Upload only new hashed files; keep existing ones (no --delete).\n",[353,361,363,367,371,374,377,380],{"class":127,"line":362},2,[353,364,366],{"class":365},"sQw3B","aws",[353,368,370],{"class":369},"sZ8jY"," s3",[353,372,373],{"class":369}," sync",[353,375,376],{"class":369}," dist\u002Fassets",[353,378,379],{"class":369}," s3:\u002F\u002Fmy-site-assets\u002Fassets",[353,381,383],{"class":382},"sPARh"," \\\n",[353,385,387,391],{"class":127,"line":386},3,[353,388,390],{"class":389},"sPXB4","  --cache-control",[353,392,393],{"class":369}," \"public, max-age=31536000, immutable\"\n",[353,395,397],{"class":127,"line":396},4,[353,398,399],{"class":358},"# HTML is uploaded separately with a short TTL so new visitors get the new build.\n",[353,401,403],{"class":127,"line":402},5,[353,404,405],{"class":358},"# trade-off: retaining old assets costs storage and keeps old code reachable.\n",[353,407,409],{"class":127,"line":408},6,[353,410,411],{"class":358},"# Expire them after a fixed window with a bucket lifecycle rule rather than never.\n",[15,413,414],{},"Expected outcome: old tabs can still load the chunks they reference; most errors disappear.",[336,416,418],{"id":417},"_2-return-real-404s-for-missing-assets","2. Return real 404s for missing assets",[15,420,421,422,424],{},"Exclude asset paths from the SPA fallback so a missing chunk gets a 404 with no body, not ",[30,423,221],{},".",[344,426,430],{"className":427,"code":428,"language":429,"meta":349,"style":349},"language-nginx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","location \u002Fassets\u002F {\n  try_files $uri =404;                     # never fall back to index.html for assets\n  add_header Cache-Control \"public, max-age=31536000, immutable\";\n}\nlocation \u002F { try_files $uri \u002Findex.html; }\n# trade-off: none worth mentioning for assets — but make sure the CDN does not\n# cache 404s for long, or a chunk uploaded seconds late stays \"missing\".\n","nginx",[30,431,432,444,461,475,480,496,501],{"__ignoreMap":349},[353,433,434,437,440],{"class":127,"line":355},[353,435,436],{"class":382},"location",[353,438,439],{"class":365}," \u002Fassets\u002F ",[353,441,443],{"class":442},"saISM","{\n",[353,445,446,449,452,455,458],{"class":127,"line":362},[353,447,448],{"class":382},"  try_files ",[353,450,451],{"class":442},"$uri ",[353,453,454],{"class":389},"=404",[353,456,457],{"class":442},";                     ",[353,459,460],{"class":358},"# never fall back to index.html for assets\n",[353,462,463,466,469,472],{"class":127,"line":386},[353,464,465],{"class":382},"  add_header ",[353,467,468],{"class":442},"Cache-Control ",[353,470,471],{"class":369},"\"public, max-age=31536000, immutable\"",[353,473,474],{"class":442},";\n",[353,476,477],{"class":127,"line":396},[353,478,479],{"class":442},"}\n",[353,481,482,484,487,490,493],{"class":127,"line":402},[353,483,436],{"class":382},[353,485,486],{"class":365}," \u002F ",[353,488,489],{"class":442},"{",[353,491,492],{"class":382}," try_files ",[353,494,495],{"class":442},"$uri \u002Findex.html; }\n",[353,497,498],{"class":127,"line":408},[353,499,500],{"class":358},"# trade-off: none worth mentioning for assets — but make sure the CDN does not\n",[353,502,504],{"class":127,"line":503},7,[353,505,506],{"class":358},"# cache 404s for long, or a chunk uploaded seconds late stays \"missing\".\n",[15,508,509],{},"Expected outcome: failures are clean and diagnosable, and caches are never poisoned with HTML.",[336,511,513],{"id":512},"_3-retry-once-then-reload-to-the-new-version","3. Retry once, then reload to the new version",[15,515,516],{},"When an import fails, retry after a short delay (transient network errors are common on mobile). If it fails again, the user's runtime is outdated: reload the page at the target URL, once.",[344,518,522],{"className":519,"code":520,"language":521,"meta":349,"style":349},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","export async function importWithRecovery(load, targetUrl) {\n  try { return await load(); }\n  catch (err) {\n    await new Promise((r) => setTimeout(r, 500));\n    try { return await load(); }\n    catch {\n      const key = 'chunk-reload:' + targetUrl;\n      if (!sessionStorage.getItem(key)) { sessionStorage.setItem(key, '1'); location.assign(targetUrl); }\n      throw err;\n    }\n  }\n}\n\u002F\u002F trade-off: a reload discards in-memory state (unsaved form input). Save\n\u002F\u002F drafts before reloading, or show a banner asking the user to refresh instead\n\u002F\u002F of reloading automatically on pages where state matters.\n","javascript",[30,523,524,553,573,581,616,631,639,659,698,707,713,719,724,730,736],{"__ignoreMap":349},[353,525,526,529,532,535,539,542,545,547,550],{"class":127,"line":355},[353,527,528],{"class":382},"export",[353,530,531],{"class":382}," async",[353,533,534],{"class":382}," function",[353,536,538],{"class":537},"smZ65"," importWithRecovery",[353,540,541],{"class":442},"(",[353,543,544],{"class":365},"load",[353,546,197],{"class":442},[353,548,549],{"class":365},"targetUrl",[353,551,552],{"class":442},") {\n",[353,554,555,558,561,564,567,570],{"class":127,"line":362},[353,556,557],{"class":382},"  try",[353,559,560],{"class":442}," { ",[353,562,563],{"class":382},"return",[353,565,566],{"class":382}," await",[353,568,569],{"class":537}," load",[353,571,572],{"class":442},"(); }\n",[353,574,575,578],{"class":127,"line":386},[353,576,577],{"class":382},"  catch",[353,579,580],{"class":442}," (err) {\n",[353,582,583,586,589,592,595,598,601,604,607,610,613],{"class":127,"line":396},[353,584,585],{"class":382},"    await",[353,587,588],{"class":382}," new",[353,590,591],{"class":389}," Promise",[353,593,594],{"class":442},"((",[353,596,597],{"class":365},"r",[353,599,600],{"class":442},") ",[353,602,603],{"class":382},"=>",[353,605,606],{"class":537}," setTimeout",[353,608,609],{"class":442},"(r, ",[353,611,612],{"class":389},"500",[353,614,615],{"class":442},"));\n",[353,617,618,621,623,625,627,629],{"class":127,"line":402},[353,619,620],{"class":382},"    try",[353,622,560],{"class":442},[353,624,563],{"class":382},[353,626,566],{"class":382},[353,628,569],{"class":537},[353,630,572],{"class":442},[353,632,633,636],{"class":127,"line":408},[353,634,635],{"class":382},"    catch",[353,637,638],{"class":442}," {\n",[353,640,641,644,647,650,653,656],{"class":127,"line":503},[353,642,643],{"class":382},"      const",[353,645,646],{"class":389}," key",[353,648,649],{"class":382}," =",[353,651,652],{"class":369}," 'chunk-reload:'",[353,654,655],{"class":382}," +",[353,657,658],{"class":442}," targetUrl;\n",[353,660,662,665,668,671,674,677,680,683,686,689,692,695],{"class":127,"line":661},8,[353,663,664],{"class":382},"      if",[353,666,667],{"class":442}," (",[353,669,670],{"class":382},"!",[353,672,673],{"class":442},"sessionStorage.",[353,675,676],{"class":537},"getItem",[353,678,679],{"class":442},"(key)) { sessionStorage.",[353,681,682],{"class":537},"setItem",[353,684,685],{"class":442},"(key, ",[353,687,688],{"class":369},"'1'",[353,690,691],{"class":442},"); location.",[353,693,694],{"class":537},"assign",[353,696,697],{"class":442},"(targetUrl); }\n",[353,699,701,704],{"class":127,"line":700},9,[353,702,703],{"class":382},"      throw",[353,705,706],{"class":442}," err;\n",[353,708,710],{"class":127,"line":709},10,[353,711,712],{"class":442},"    }\n",[353,714,716],{"class":127,"line":715},11,[353,717,718],{"class":442},"  }\n",[353,720,722],{"class":127,"line":721},12,[353,723,479],{"class":442},[353,725,727],{"class":127,"line":726},13,[353,728,729],{"class":358},"\u002F\u002F trade-off: a reload discards in-memory state (unsaved form input). Save\n",[353,731,733],{"class":127,"line":732},14,[353,734,735],{"class":358},"\u002F\u002F drafts before reloading, or show a banner asking the user to refresh instead\n",[353,737,739],{"class":127,"line":738},15,[353,740,741],{"class":358},"\u002F\u002F of reloading automatically on pages where state matters.\n",[15,743,744],{},"Expected outcome: the rare user who still hits a missing chunk lands on the new version of the page they wanted, instead of a dead click.",[336,746,748],{"id":747},"_4-hook-recovery-into-the-router","4. Hook recovery into the router",[15,750,751,752,755,756,759,760,763,764,767],{},"Vue Router exposes ",[30,753,754],{},"router.onError","; React Router data routers expose ",[30,757,758],{},"errorElement","; Vite emits a ",[30,761,762],{},"vite:preloadError"," event on ",[30,765,766],{},"window"," when a dynamic import's preload fails.",[344,769,771],{"className":519,"code":770,"language":521,"meta":349,"style":349},"window.addEventListener('vite:preloadError', (event) => {\n  event.preventDefault();                       \u002F\u002F stop the error propagating\n  location.reload();\n});\nrouter.onError((err, to) => {\n  if (\u002FFailed to fetch dynamically imported module|ChunkLoadError\u002F.test(String(err))) location.assign(to.fullPath);\n});\n\u002F\u002F trade-off: blanket reloads on any import error can loop if the error is\n\u002F\u002F not version skew (a genuinely broken chunk). Guard with a session flag as above.\n",[30,772,773,798,812,823,828,852,886,890,895],{"__ignoreMap":349},[353,774,775,778,781,783,786,789,792,794,796],{"class":127,"line":355},[353,776,777],{"class":442},"window.",[353,779,780],{"class":537},"addEventListener",[353,782,541],{"class":442},[353,784,785],{"class":369},"'vite:preloadError'",[353,787,788],{"class":442},", (",[353,790,791],{"class":365},"event",[353,793,600],{"class":442},[353,795,603],{"class":382},[353,797,638],{"class":442},[353,799,800,803,806,809],{"class":127,"line":362},[353,801,802],{"class":442},"  event.",[353,804,805],{"class":537},"preventDefault",[353,807,808],{"class":442},"();                       ",[353,810,811],{"class":358},"\u002F\u002F stop the error propagating\n",[353,813,814,817,820],{"class":127,"line":386},[353,815,816],{"class":442},"  location.",[353,818,819],{"class":537},"reload",[353,821,822],{"class":442},"();\n",[353,824,825],{"class":127,"line":396},[353,826,827],{"class":442},"});\n",[353,829,830,833,836,838,841,843,846,848,850],{"class":127,"line":402},[353,831,832],{"class":442},"router.",[353,834,835],{"class":537},"onError",[353,837,594],{"class":442},[353,839,840],{"class":365},"err",[353,842,197],{"class":442},[353,844,845],{"class":365},"to",[353,847,600],{"class":442},[353,849,603],{"class":382},[353,851,638],{"class":442},[353,853,854,857,859,862,865,868,870,873,875,878,881,883],{"class":127,"line":408},[353,855,856],{"class":382},"  if",[353,858,667],{"class":442},[353,860,861],{"class":369},"\u002FFailed to fetch dynamically imported module",[353,863,864],{"class":382},"|",[353,866,867],{"class":369},"ChunkLoadError\u002F",[353,869,424],{"class":442},[353,871,872],{"class":537},"test",[353,874,541],{"class":442},[353,876,877],{"class":537},"String",[353,879,880],{"class":442},"(err))) location.",[353,882,694],{"class":537},[353,884,885],{"class":442},"(to.fullPath);\n",[353,887,888],{"class":127,"line":503},[353,889,827],{"class":442},[353,891,892],{"class":127,"line":661},[353,893,894],{"class":358},"\u002F\u002F trade-off: blanket reloads on any import error can loop if the error is\n",[353,896,897],{"class":127,"line":700},[353,898,899],{"class":358},"\u002F\u002F not version skew (a genuinely broken chunk). Guard with a session flag as above.\n",[15,901,902],{},[44,903,51,906,51,909,51,912,51,914,51,917,51,919,51,925,51,931,51,935,51,938,51,942,51,945,51,948,51,952,51,956,51,959,51,963,51,967,51,973,51,977,51,981,51,986,51,988,51,991,51,995,51,998,51,1001,51,1004,51],{"viewBox":904,"width":47,"role":48,"ariaLabel":905,"style":50},"0 0 760 318","Four layers that together prevent chunk load errors after deploys from breaking user navigation.",[53,907],{"className":908,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],[60,910,911],{},"Layers of protection against version skew",[64,913,905],{},[53,915],{"x":86,"y":86,"width":87,"height":916,"rx":89,"fill":90,"stroke":82,"style":91},"316",[93,918,911],{"x":95,"y":96,"fill":82,"style":97},[53,920],{"x":921,"y":101,"width":922,"height":923,"rx":103,"fill":314,"stroke":314,"style":924},"72.0","660.0","51.0","fill-opacity:0.14;stroke-opacity:0.9",[93,926,930],{"x":927,"y":928,"fill":82,"style":109,"textAnchor":929},"86.0","77.0","start","Additive deploys with retention",[93,932,934],{"x":927,"y":933,"fill":82,"style":151,"textAnchor":929},"94.0","Old hashed assets stay available for days",[53,936],{"x":921,"y":937,"width":922,"height":923,"rx":103,"fill":314,"stroke":314,"style":924},"119.0",[93,939,941],{"x":927,"y":940,"fill":82,"style":109,"textAnchor":929},"140.0","Real 404s for missing assets",[93,943,944],{"x":927,"y":160,"fill":82,"style":151,"textAnchor":929},"No SPA fallback HTML served as JavaScript",[53,946],{"x":921,"y":947,"width":922,"height":923,"rx":103,"fill":314,"stroke":314,"style":924},"182.0",[93,949,951],{"x":927,"y":950,"fill":82,"style":109,"textAnchor":929},"203.0","Retry then reload once",[93,953,955],{"x":927,"y":954,"fill":82,"style":151,"textAnchor":929},"220.0","Transient failures retried; version skew reloads to the new build",[53,957],{"x":921,"y":958,"width":922,"height":923,"rx":103,"fill":314,"stroke":314,"style":924},"245.0",[93,960,962],{"x":927,"y":961,"fill":82,"style":109,"textAnchor":929},"266.0","Proactive update prompts",[93,964,966],{"x":927,"y":965,"fill":82,"style":151,"textAnchor":929},"283.0","Detect a new version and offer a refresh before it breaks",[127,968],{"x1":969,"y1":970,"x2":969,"y2":971,"stroke":82,"strokeWidth":131,"style":972},"43.0","95.5","130.5","stroke-opacity:0.3",[127,974],{"x1":969,"y1":975,"x2":969,"y2":976,"stroke":82,"strokeWidth":131,"style":972},"158.5","193.5",[127,978],{"x1":969,"y1":979,"x2":969,"y2":980,"stroke":82,"strokeWidth":131,"style":972},"221.5","256.5",[982,983],"circle",{"cx":969,"cy":984,"r":985,"fill":314},"81.5","13",[93,987,86],{"x":969,"y":927,"fill":58,"style":109,"textAnchor":110},[982,989],{"cx":969,"cy":990,"r":985,"fill":314},"144.5",[93,992,994],{"x":969,"y":993,"fill":58,"style":109,"textAnchor":110},"149.0","2",[982,996],{"cx":969,"cy":997,"r":985,"fill":314},"207.5",[93,999,297],{"x":969,"y":1000,"fill":58,"style":109,"textAnchor":110},"212.0",[982,1002],{"cx":969,"cy":1003,"r":985,"fill":314},"270.5",[93,1005,133],{"x":969,"y":1006,"fill":58,"style":109,"textAnchor":110},"275.0",[180,1008,1010],{"id":1009},"verification","Verification",[15,1012,1013],{},"Deploy to staging, keep a tab open from the previous build, deploy again, then navigate in the old tab to a route whose chunk changed. With retention, the old chunk should load. Delete it manually to simulate expiry and confirm the reload-once recovery lands on the new page. In production, chart chunk load errors per session against deploy times; after the fix the post-deploy spikes should flatten.",[180,1015,1017],{"id":1016},"worked-example-a-dashboard-deployed-ten-times-a-day","Worked Example: A Dashboard Deployed Ten Times a Day",[15,1019,1020,1021,1023,1024,1027,1028,1030],{},"An analytics dashboard deployed frequently and replaced its asset folder on each release. Users kept tabs open all day; error monitoring logged thousands of ",[30,1022,36],{},"s daily, and support received \"buttons stop working in the afternoon\" complaints. The team switched to additive uploads with a 14-day lifecycle rule, excluded ",[30,1025,1026],{},"\u002Fassets\u002F"," from the SPA fallback, and added the ",[30,1029,762],{}," handler with a once-per-session reload guard. Daily chunk errors fell by more than 99%, and the remaining handful corresponded to tabs older than two weeks — which now reloaded cleanly to the current build.",[180,1032,1034],{"id":1033},"common-mistakes","Common Mistakes",[185,1036,1037,1043,1049,1055],{},[188,1038,1039,1042],{},[191,1040,1041],{},"Treating chunk errors as noise."," Each one is a user whose click did nothing.",[188,1044,1045,1048],{},[191,1046,1047],{},"Reloading without a guard."," A genuinely broken chunk then causes an infinite reload loop.",[188,1050,1051,1054],{},[191,1052,1053],{},"Caching index.html for long."," New visitors keep receiving the old shell and its old chunk names, extending the skew window.",[188,1056,1057,1060,1061,424],{},[191,1058,1059],{},"Forgetting the service worker."," A precached old shell keeps referencing old chunks; update the service worker promptly — see ",[19,1062,1064],{"href":1063},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fhandling-service-worker-updates-with-skipwaiting\u002F","handling service worker updates with skipWaiting",[180,1066,1068],{"id":1067},"faq","FAQ",[1070,1071,1074,1078],"details",{"className":1072},[1073],"faq-item",[1075,1076,1077],"summary",{},"Can I avoid the problem by not hashing filenames?",[15,1079,1080,1081,1085],{},"Stable filenames make old tabs load ",[1082,1083,1084],"em",{},"new"," code into an old runtime, which is worse: mismatched module versions produce subtle bugs. Hashing plus retention is the correct combination; it keeps each runtime consistent with the chunks it expects.",[1070,1087,1089,1092],{"className":1088},[1073],[1075,1090,1091],{},"Should I prompt users to refresh when a new version is deployed?",[15,1093,1094,1095,1098],{},"It is a good complement. Poll a small ",[30,1096,1097],{},"version.json"," (or listen for a service worker update) and show a non-blocking \"A new version is available\" banner. Users refresh at a moment that suits them, before version skew can break anything.",[1070,1100,1102,1105],{"className":1101},[1073],[1075,1103,1104],{},"Do CDNs need special configuration?",[15,1106,1107,1108,1112],{},"Make sure 404s for asset paths are cached briefly or not at all, and that purges on deploy target HTML rather than hashed assets. Purging old hashed assets defeats retention. ",[19,1109,1111],{"href":1110},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F","Invalidating immutable hashed assets safely"," covers the caching side.",[180,1114,1116],{"id":1115},"related","Related",[185,1118,1119,1126,1133],{},[188,1120,1121,1125],{},[19,1122,1124],{"href":1123},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F","Fixing stale HTML after a deploy"," — the HTML side of version skew.",[188,1127,1128,1132],{},[19,1129,1131],{"href":1130},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fcode-splitting-vue-router-routes\u002F","Code-splitting Vue Router routes"," — where router error hooks fit.",[188,1134,1135,1139],{},[19,1136,1138],{"href":1137},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense\u002F","Lazy-loading React components with Suspense"," — error boundaries around lazy components.",[1141,1142,1144],"script",{"type":1143},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Handle Chunk Load Errors After a Deploy\",\n  \"description\": \"Why deploys break lazy loading for users with open tabs, and the asset retention, retry and reload strategies that prevent it.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Keep previous builds' assets available\",\n      \"text\": \"Deploy hashed assets additively: upload new files, never delete recent old ones.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Return real 404s for missing assets\",\n      \"text\": \"Exclude asset paths from the SPA fallback so a missing chunk gets a 404 with no body, not index.html.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Retry once, then reload to the new version\",\n      \"text\": \"When an import fails, retry after a short delay (transient network errors are common on mobile).\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Hook recovery into the router\",\n      \"text\": \"Vue Router exposes router.onError; React Router data routers expose errorElement; Vite emits a vite:preloadError event on window when a dynamic import's preload fails.\"\n    }\n  ]\n}\n",[1141,1146,1147],{"type":1143},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Handle Chunk Load Errors After a Deploy\",\n  \"description\": \"Why deploys break lazy loading for users with open tabs, and the asset retention, retry and reload strategies that prevent it.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy\u002F\"\n  }\n}\n",[1141,1149,1150],{"type":1143},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"JavaScript Bundle Optimization & Code Splitting\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Dynamic Imports and Route-Based Splitting\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Handling Chunk Load Errors After a Deploy\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy\u002F\"\n    }\n  ]\n}\n",[1152,1153,1154],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}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);}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}",{"title":349,"searchDepth":362,"depth":362,"links":1156},[1157,1158,1159,1165,1166,1167,1168,1169],{"id":182,"depth":362,"text":183},{"id":237,"depth":362,"text":238},{"id":333,"depth":362,"text":334,"children":1160},[1161,1162,1163,1164],{"id":338,"depth":386,"text":339},{"id":417,"depth":386,"text":418},{"id":512,"depth":386,"text":513},{"id":747,"depth":386,"text":748},{"id":1009,"depth":362,"text":1010},{"id":1016,"depth":362,"text":1017},{"id":1033,"depth":362,"text":1034},{"id":1067,"depth":362,"text":1068},{"id":1115,"depth":362,"text":1116},"Why deploys break lazy loading for users with open tabs, and the asset retention, retry and reload strategies that prevent it.","md",{"slug":1173,"type":1174,"breadcrumb":1175,"datePublished":1183,"dateModified":1183},"handling-chunk-load-errors-after-a-deploy","article",[1176,1179,1180,1181],{"name":1177,"url":1178},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1182},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy\u002F","2026-10-06",true,"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy",{"title":1187,"description":1188},"Handling ChunkLoadError After a Deploy","Old tabs request code-split chunks that a new deploy removed, causing ChunkLoadError and broken navigation. Keep old assets, retry, and reload safely.","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy\u002Findex","JOCwyVsHozzMjLtrgbqP8Wa3o3h7rEWTp4X_2Xsk6q8",[1192,1196],{"title":1193,"path":1194,"stem":1195},"Fixing Waterfalls from Nested Dynamic Imports","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002Findex",{"title":1197,"path":1198,"stem":1199},"Route-Level Code Splitting in Next.js","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fimplementing-route-level-code-splitting-in-nextjs","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fimplementing-route-level-code-splitting-in-nextjs\u002Findex",1791308076266]