[{"data":1,"prerenderedAt":1347},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports":1339},{"id":4,"title":5,"body":6,"description":1319,"extension":1320,"meta":1321,"navigation":489,"path":1333,"seo":1334,"stem":1337,"__hash__":1338},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002Findex.md","Fixing Waterfalls from Nested Dynamic Imports",{"type":7,"value":8,"toc":1311},"minimark",[9,14,47,50,55,92,228,232,238,251,257,266,270,275,429,436,441,574,577,583,698,708,904,908,911,914,1196,1199,1203,1215,1228,1240,1249,1262,1271,1275,1296,1301,1304,1307],[10,11,13],"h1",{"id":12},"how-to-fix-request-waterfalls-caused-by-nested-dynamic-imports","How to Fix Request Waterfalls Caused by Nested Dynamic Imports",[15,16,17,18,23,24,28,29,33,34,37,38,42,43,46],"p",{},"This walkthrough extends ",[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",". Splitting reduced the initial bundle from ",[30,31,32],"code",{},"480KB"," to ",[30,35,36],{},"160KB",", and navigation got ",[39,40,41],"em",{},"slower",". The route chunk imports a data layer chunk, which imports a chart library chunk, and each one can only be discovered after the previous has downloaded and executed. Three sequential round trips on a ",[30,44,45],{},"170ms"," connection is over half a second of nothing happening.",[15,48,49],{},"Code splitting trades bytes for round trips. The fix is to keep the byte saving while removing the serialisation.",[51,52,54],"h2",{"id":53},"rapid-diagnosis-spot-the-staircase","Rapid Diagnosis: Spot the Staircase",[56,57,58,66,76,82],"ul",{},[59,60,61,65],"li",{},[62,63,64],"strong",{},"Look at the Network waterfall shape."," Chunks that start in a descending staircase, each beginning as the previous ends, are dependent — not parallel.",[59,67,68,71,72,75],{},[62,69,70],{},"Read the initiator chain."," Click a chunk and check ",[62,73,74],{},"Initiator",": if it is another chunk rather than the document or the entry, you have nesting.",[59,77,78,81],{},[62,79,80],{},"Count the hops to first render."," Any route needing more than two sequential chunk fetches will feel slow regardless of total size.",[59,83,84,87,88,91],{},[62,85,86],{},"Check whether the leaf is small."," Serialising a round trip to fetch ",[30,89,90],{},"4KB"," is the clearest case for inlining it into its parent.",[93,94,101,102,109,110,109,114,109,118,109,127,109,134,109,139,109,147,109,154,109,158,109,163,109,170,109,175,109,182,109,188,109,193,109,197,109,201,109,205,109,209,109,212,109,215,109,218,109,224,109],"svg",{"xmlns":95,"viewBox":96,"width":97,"role":98,"ariaLabel":99,"style":100},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 250","100%","img","Two waterfalls: a serialised chain of three chunks each starting after the previous finishes, versus the same three chunks fetched in parallel after flattening and preloading","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[103,104],"rect",{"className":105,"x":107,"y":107,"width":97,"height":97,"fill":108},[106],"svg-canvas","0","#ffffff"," ",[111,112,113],"title",{},"Serialised chunk chain versus parallel fetch",[115,116,117],"desc",{},"Three dependent chunks fetched in sequence take 540 milliseconds; the same three fetched in parallel after flattening take 190.",[103,119],{"x":120,"y":120,"width":121,"height":122,"rx":123,"fill":124,"stroke":125,"style":126},"1","758","248","10","none","currentColor","stroke-opacity:0.18",[128,129,133],"text",{"x":130,"y":131,"fill":125,"style":132},"24","32","font-size:16px;font-weight:700","Three chunks: in sequence or together",[128,135,138],{"x":130,"y":136,"fill":125,"style":137},"72","font-size:11.5px;font-weight:700","Nested",[103,140],{"x":141,"y":142,"width":143,"height":130,"rx":144,"fill":145,"stroke":145,"style":146},"110","56","150","3","#0466c8","fill-opacity:0.2;stroke-opacity:0.55",[128,148,153],{"x":149,"y":150,"fill":125,"style":151,"textAnchor":152},"185","73","font-size:10px;","middle","route.js",[103,155],{"x":156,"y":157,"width":143,"height":130,"rx":144,"fill":145,"stroke":145,"style":146},"260","86",[128,159,162],{"x":160,"y":161,"fill":125,"style":151,"textAnchor":152},"335","103","data-layer.js",[103,164],{"x":165,"y":166,"width":143,"height":130,"rx":144,"fill":167,"stroke":168,"style":169},"410","116","#ffc300","#b8860b","fill-opacity:0.3",[128,171,174],{"x":172,"y":173,"fill":125,"style":151,"textAnchor":152},"485","133","charts.js",[176,177],"line",{"x1":178,"y1":179,"x2":180,"y2":179,"stroke":168,"style":181},"560","128","590","stroke-opacity:0.8",[128,183,187],{"x":184,"y":185,"fill":168,"style":186},"600","132","font-size:11px;font-weight:700","540ms",[176,189],{"x1":130,"y1":190,"x2":191,"y2":190,"stroke":125,"style":192},"160","736","stroke-opacity:0.2",[128,194,196],{"x":130,"y":195,"fill":125,"style":137},"196","Flattened",[103,198],{"x":141,"y":199,"width":143,"height":200,"rx":144,"fill":145,"stroke":145,"style":146},"180","18",[128,202,153],{"x":149,"y":203,"fill":125,"style":204,"textAnchor":152},"193","font-size:9.5px;",[103,206],{"x":141,"y":207,"width":208,"height":200,"rx":144,"fill":145,"stroke":145,"style":146},"202","140",[128,210,162],{"x":199,"y":211,"fill":125,"style":204,"textAnchor":152},"215",[103,213],{"x":141,"y":214,"width":143,"height":200,"rx":144,"fill":145,"stroke":145,"style":146},"224",[128,216,174],{"x":149,"y":217,"fill":125,"style":204,"textAnchor":152},"237",[176,219],{"x1":220,"y1":221,"x2":222,"y2":221,"stroke":125,"style":223},"262","211","292","stroke-opacity:0.6",[128,225,227],{"x":226,"y":211,"fill":125,"style":186},"302","190ms",[51,229,231],{"id":230},"root-cause-analysis","Root Cause Analysis",[15,233,234,237],{},[62,235,236],{},"1. Discovery is sequential by construction."," The bundler cannot emit a request for a chunk whose import lives inside another chunk's code — it must download and execute the parent first. Every level of nesting is one more round trip.",[15,239,240,243,244,246,247,250],{},[62,241,242],{},"2. Over-splitting small modules."," A ",[30,245,90],{}," helper in its own chunk costs a full round trip to save four kilobytes. Below roughly ",[30,248,249],{},"20KB",", the round trip usually dominates.",[15,252,253,256],{},[62,254,255],{},"3. Import-on-render inside the component."," A dynamic import in a component body executes only after the parent has rendered, adding the render pass to the chain.",[15,258,259,109,262,265],{},[62,260,261],{},"4. Awaiting imports in series.",[30,263,264],{},"await import(a); await import(b);"," serialises two independent fetches for no reason — a one-character fix with a large effect.",[51,267,269],{"id":268},"step-by-step-resolution","Step-by-Step Resolution",[15,271,272],{},[62,273,274],{},"1. Merge leaves that are too small to deserve a chunk.",[276,277,282],"pre",{"className":278,"code":279,"language":280,"meta":281,"style":281},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F vite\u002Frollup: keep small shared helpers with their consumer.\nexport default {\n  build: { rollupOptions: { output: {\n    manualChunks(id) {\n      if (id.includes('node_modules\u002Fchart.js')) return 'charts';   \u002F\u002F big, shared\n      return undefined;                                            \u002F\u002F everything else inlines\n    },\n    experimentalMinChunkSize: 20_000        \u002F\u002F fold anything smaller into its parent\n  } } }\n};\n\u002F\u002F trade-off: bigger chunks mean more re-downloaded bytes when any part changes.\n\u002F\u002F With content-hashed filenames and long cache lifetimes that cost is paid\n\u002F\u002F rarely; a round trip is paid on every cold navigation.\n","javascript","",[30,283,284,292,306,313,329,362,378,384,396,402,411,417,423],{"__ignoreMap":281},[285,286,288],"span",{"class":176,"line":287},1,[285,289,291],{"class":290},"sjfSM","\u002F\u002F vite\u002Frollup: keep small shared helpers with their consumer.\n",[285,293,295,299,302],{"class":176,"line":294},2,[285,296,298],{"class":297},"sPARh","export",[285,300,301],{"class":297}," default",[285,303,305],{"class":304},"sQw3B"," {\n",[285,307,309],{"class":176,"line":308},3,[285,310,312],{"class":311},"saISM","  build: { rollupOptions: { output: {\n",[285,314,316,320,323,326],{"class":176,"line":315},4,[285,317,319],{"class":318},"smZ65","    manualChunks",[285,321,322],{"class":311},"(",[285,324,325],{"class":304},"id",[285,327,328],{"class":311},") {\n",[285,330,332,335,338,341,343,347,350,353,356,359],{"class":176,"line":331},5,[285,333,334],{"class":297},"      if",[285,336,337],{"class":311}," (id.",[285,339,340],{"class":318},"includes",[285,342,322],{"class":311},[285,344,346],{"class":345},"sZ8jY","'node_modules\u002Fchart.js'",[285,348,349],{"class":311},")) ",[285,351,352],{"class":297},"return",[285,354,355],{"class":345}," 'charts'",[285,357,358],{"class":311},";   ",[285,360,361],{"class":290},"\u002F\u002F big, shared\n",[285,363,365,368,372,375],{"class":176,"line":364},6,[285,366,367],{"class":297},"      return",[285,369,371],{"class":370},"sPXB4"," undefined",[285,373,374],{"class":311},";                                            ",[285,376,377],{"class":290},"\u002F\u002F everything else inlines\n",[285,379,381],{"class":176,"line":380},7,[285,382,383],{"class":311},"    },\n",[285,385,387,390,393],{"class":176,"line":386},8,[285,388,389],{"class":311},"    experimentalMinChunkSize: ",[285,391,392],{"class":370},"20_000",[285,394,395],{"class":290},"        \u002F\u002F fold anything smaller into its parent\n",[285,397,399],{"class":176,"line":398},9,[285,400,401],{"class":311},"  } } }\n",[285,403,405,408],{"class":176,"line":404},10,[285,406,407],{"class":304},"}",[285,409,410],{"class":311},";\n",[285,412,414],{"class":176,"line":413},11,[285,415,416],{"class":290},"\u002F\u002F trade-off: bigger chunks mean more re-downloaded bytes when any part changes.\n",[285,418,420],{"class":176,"line":419},12,[285,421,422],{"class":290},"\u002F\u002F With content-hashed filenames and long cache lifetimes that cost is paid\n",[285,424,426],{"class":176,"line":425},13,[285,427,428],{"class":290},"\u002F\u002F rarely; a round trip is paid on every cold navigation.\n",[15,430,431,432,435],{},"Expected outcome: the chain shortens by one hop per merged leaf, typically ",[30,433,434],{},"120–200ms"," each on mobile.",[15,437,438],{},[62,439,440],{},"2. Hoist the nested import to the parent and fetch in parallel.",[276,442,444],{"className":278,"code":443,"language":280,"meta":281,"style":281},"\u002F\u002F Before: charts.js is discovered only after data-layer.js executes.\nconst { loadData } = await import('.\u002Fdata-layer.js');   \u002F\u002F which imports charts\n\n\u002F\u002F After: both requests start together.\nconst [{ loadData }, { renderChart }] = await Promise.all([\n  import('.\u002Fdata-layer.js'),\n  import('.\u002Fcharts.js')\n]);\n\u002F\u002F trade-off: you now always fetch charts.js, even on the paths that would not\n\u002F\u002F have needed it. Only hoist when the leaf is needed on the common path —\n\u002F\u002F otherwise keep the lazy import and preload it instead (step 3).\n",[30,445,446,451,485,491,496,530,542,554,559,564,569],{"__ignoreMap":281},[285,447,448],{"class":176,"line":287},[285,449,450],{"class":290},"\u002F\u002F Before: charts.js is discovered only after data-layer.js executes.\n",[285,452,453,456,459,462,465,468,471,474,476,479,482],{"class":176,"line":294},[285,454,455],{"class":297},"const",[285,457,458],{"class":311}," { ",[285,460,461],{"class":370},"loadData",[285,463,464],{"class":311}," } ",[285,466,467],{"class":297},"=",[285,469,470],{"class":297}," await",[285,472,473],{"class":297}," import",[285,475,322],{"class":311},[285,477,478],{"class":345},"'.\u002Fdata-layer.js'",[285,480,481],{"class":311},");   ",[285,483,484],{"class":290},"\u002F\u002F which imports charts\n",[285,486,487],{"class":176,"line":308},[285,488,490],{"emptyLinePlaceholder":489},true,"\n",[285,492,493],{"class":176,"line":315},[285,494,495],{"class":290},"\u002F\u002F After: both requests start together.\n",[285,497,498,500,503,505,508,511,514,516,518,521,524,527],{"class":176,"line":331},[285,499,455],{"class":297},[285,501,502],{"class":311}," [{ ",[285,504,461],{"class":370},[285,506,507],{"class":311}," }, { ",[285,509,510],{"class":370},"renderChart",[285,512,513],{"class":311}," }] ",[285,515,467],{"class":297},[285,517,470],{"class":297},[285,519,520],{"class":370}," Promise",[285,522,523],{"class":311},".",[285,525,526],{"class":318},"all",[285,528,529],{"class":311},"([\n",[285,531,532,535,537,539],{"class":176,"line":364},[285,533,534],{"class":297},"  import",[285,536,322],{"class":311},[285,538,478],{"class":345},[285,540,541],{"class":311},"),\n",[285,543,544,546,548,551],{"class":176,"line":380},[285,545,534],{"class":297},[285,547,322],{"class":311},[285,549,550],{"class":345},"'.\u002Fcharts.js'",[285,552,553],{"class":311},")\n",[285,555,556],{"class":176,"line":386},[285,557,558],{"class":311},"]);\n",[285,560,561],{"class":176,"line":398},[285,562,563],{"class":290},"\u002F\u002F trade-off: you now always fetch charts.js, even on the paths that would not\n",[285,565,566],{"class":176,"line":404},[285,567,568],{"class":290},"\u002F\u002F have needed it. Only hoist when the leaf is needed on the common path —\n",[285,570,571],{"class":176,"line":413},[285,572,573],{"class":290},"\u002F\u002F otherwise keep the lazy import and preload it instead (step 3).\n",[15,575,576],{},"Expected outcome: two round trips collapse into one, cutting the chain's wall-clock roughly in half.",[15,578,579,582],{},[62,580,581],{},"3. Preload the next hop while the current one executes."," Where the leaf is genuinely conditional, start it early without awaiting it.",[276,584,586],{"className":278,"code":585,"language":280,"meta":281,"style":281},"\u002F\u002F Warm the next chunk during the parent's execution, without blocking on it.\nconst chartsPromise = import('.\u002Fcharts.js');           \u002F\u002F fire, do not await\nconst { rows } = await loadData();\nif (rows.length) {\n  const { renderChart } = await chartsPromise;         \u002F\u002F already in flight\n  renderChart(rows);\n}\n\u002F\u002F trade-off: an un-awaited import that is never used still downloads. Keep the\n\u002F\u002F speculative fetch to one hop ahead, and skip it when navigator.connection\n\u002F\u002F reports saveData.\n",[30,587,588,593,615,636,649,670,678,683,688,693],{"__ignoreMap":281},[285,589,590],{"class":176,"line":287},[285,591,592],{"class":290},"\u002F\u002F Warm the next chunk during the parent's execution, without blocking on it.\n",[285,594,595,597,600,603,605,607,609,612],{"class":176,"line":294},[285,596,455],{"class":297},[285,598,599],{"class":370}," chartsPromise",[285,601,602],{"class":297}," =",[285,604,473],{"class":297},[285,606,322],{"class":311},[285,608,550],{"class":345},[285,610,611],{"class":311},");           ",[285,613,614],{"class":290},"\u002F\u002F fire, do not await\n",[285,616,617,619,621,624,626,628,630,633],{"class":176,"line":308},[285,618,455],{"class":297},[285,620,458],{"class":311},[285,622,623],{"class":370},"rows",[285,625,464],{"class":311},[285,627,467],{"class":297},[285,629,470],{"class":297},[285,631,632],{"class":318}," loadData",[285,634,635],{"class":311},"();\n",[285,637,638,641,644,647],{"class":176,"line":315},[285,639,640],{"class":297},"if",[285,642,643],{"class":311}," (rows.",[285,645,646],{"class":370},"length",[285,648,328],{"class":311},[285,650,651,654,656,658,660,662,664,667],{"class":176,"line":331},[285,652,653],{"class":297},"  const",[285,655,458],{"class":311},[285,657,510],{"class":370},[285,659,464],{"class":311},[285,661,467],{"class":297},[285,663,470],{"class":297},[285,665,666],{"class":311}," chartsPromise;         ",[285,668,669],{"class":290},"\u002F\u002F already in flight\n",[285,671,672,675],{"class":176,"line":364},[285,673,674],{"class":318},"  renderChart",[285,676,677],{"class":311},"(rows);\n",[285,679,680],{"class":176,"line":380},[285,681,682],{"class":311},"}\n",[285,684,685],{"class":176,"line":386},[285,686,687],{"class":290},"\u002F\u002F trade-off: an un-awaited import that is never used still downloads. Keep the\n",[285,689,690],{"class":176,"line":398},[285,691,692],{"class":290},"\u002F\u002F speculative fetch to one hop ahead, and skip it when navigator.connection\n",[285,694,695],{"class":176,"line":404},[285,696,697],{"class":290},"\u002F\u002F reports saveData.\n",[15,699,700,703,704,523],{},[62,701,702],{},"4. Prefetch route chunks on intent, not on load."," Hover, focus or viewport entry gives you the chunk before the click without competing with the current page — the same technique described for ",[19,705,707],{"href":706},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fimplementing-route-level-code-splitting-in-nextjs\u002F","route-level splitting in Next.js",[276,709,711],{"className":278,"code":710,"language":280,"meta":281,"style":281},"const seen = new Set();\ndocument.addEventListener('pointerenter', (e) => {\n  const a = e.target.closest?.('a[data-route-chunk]');\n  if (!a || seen.has(a.dataset.routeChunk)) return;\n  seen.add(a.dataset.routeChunk);\n  const l = document.createElement('link');\n  l.rel = 'modulepreload'; l.href = a.dataset.routeChunk;\n  document.head.append(l);\n}, { capture: true, passive: true });\n\u002F\u002F trade-off: modulepreload parses the module graph as well as fetching it,\n\u002F\u002F which costs main-thread time on hover. On a dense navigation menu that adds\n\u002F\u002F up — gate it to links the pointer rests on for a moment.\n",[30,712,713,730,757,781,811,822,844,862,873,889,894,899],{"__ignoreMap":281},[285,714,715,717,720,722,725,728],{"class":176,"line":287},[285,716,455],{"class":297},[285,718,719],{"class":370}," seen",[285,721,602],{"class":297},[285,723,724],{"class":297}," new",[285,726,727],{"class":318}," Set",[285,729,635],{"class":311},[285,731,732,735,738,740,743,746,749,752,755],{"class":176,"line":294},[285,733,734],{"class":311},"document.",[285,736,737],{"class":318},"addEventListener",[285,739,322],{"class":311},[285,741,742],{"class":345},"'pointerenter'",[285,744,745],{"class":311},", (",[285,747,748],{"class":304},"e",[285,750,751],{"class":311},") ",[285,753,754],{"class":297},"=>",[285,756,305],{"class":311},[285,758,759,761,764,766,769,772,775,778],{"class":176,"line":308},[285,760,653],{"class":297},[285,762,763],{"class":370}," a",[285,765,602],{"class":297},[285,767,768],{"class":311}," e.target.",[285,770,771],{"class":318},"closest",[285,773,774],{"class":311},"?.(",[285,776,777],{"class":345},"'a[data-route-chunk]'",[285,779,780],{"class":311},");\n",[285,782,783,786,789,792,795,798,801,804,807,809],{"class":176,"line":315},[285,784,785],{"class":297},"  if",[285,787,788],{"class":311}," (",[285,790,791],{"class":297},"!",[285,793,794],{"class":311},"a ",[285,796,797],{"class":297},"||",[285,799,800],{"class":311}," seen.",[285,802,803],{"class":318},"has",[285,805,806],{"class":311},"(a.dataset.routeChunk)) ",[285,808,352],{"class":297},[285,810,410],{"class":311},[285,812,813,816,819],{"class":176,"line":331},[285,814,815],{"class":311},"  seen.",[285,817,818],{"class":318},"add",[285,820,821],{"class":311},"(a.dataset.routeChunk);\n",[285,823,824,826,829,831,834,837,839,842],{"class":176,"line":364},[285,825,653],{"class":297},[285,827,828],{"class":370}," l",[285,830,602],{"class":297},[285,832,833],{"class":311}," document.",[285,835,836],{"class":318},"createElement",[285,838,322],{"class":311},[285,840,841],{"class":345},"'link'",[285,843,780],{"class":311},[285,845,846,849,851,854,857,859],{"class":176,"line":380},[285,847,848],{"class":311},"  l.rel ",[285,850,467],{"class":297},[285,852,853],{"class":345}," 'modulepreload'",[285,855,856],{"class":311},"; l.href ",[285,858,467],{"class":297},[285,860,861],{"class":311}," a.dataset.routeChunk;\n",[285,863,864,867,870],{"class":176,"line":386},[285,865,866],{"class":311},"  document.head.",[285,868,869],{"class":318},"append",[285,871,872],{"class":311},"(l);\n",[285,874,875,878,881,884,886],{"class":176,"line":398},[285,876,877],{"class":311},"}, { capture: ",[285,879,880],{"class":370},"true",[285,882,883],{"class":311},", passive: ",[285,885,880],{"class":370},[285,887,888],{"class":311}," });\n",[285,890,891],{"class":176,"line":404},[285,892,893],{"class":290},"\u002F\u002F trade-off: modulepreload parses the module graph as well as fetching it,\n",[285,895,896],{"class":176,"line":413},[285,897,898],{"class":290},"\u002F\u002F which costs main-thread time on hover. On a dense navigation menu that adds\n",[285,900,901],{"class":176,"line":419},[285,902,903],{"class":290},"\u002F\u002F up — gate it to links the pointer rests on for a moment.\n",[51,905,907],{"id":906},"verification","Verification",[15,909,910],{},"Re-record the navigation and check the waterfall shape rather than the total bytes. Success looks like chunks starting at the same moment, not a staircase, and time-to-interactive on the route dropping by roughly the round trips you removed.",[15,912,913],{},"Assert the structural property in CI so a new nested import cannot quietly reintroduce the chain:",[276,915,917],{"className":278,"code":916,"language":280,"meta":281,"style":281},"\u002F\u002F Fail the build if any route needs more than two sequential chunk fetches.\nimport { readFileSync } from 'node:fs';\nconst graph = JSON.parse(readFileSync('dist\u002Fstats.json', 'utf8'));\nconst depth = (id, seen = new Set()) => {\n  if (seen.has(id)) return 0;\n  seen.add(id);\n  const kids = graph.chunks[id]?.dynamicImports ?? [];\n  return kids.length ? 1 + Math.max(...kids.map((k) => depth(k, seen))) : 0;\n};\nfor (const entry of graph.entrypoints) {\n  if (depth(entry) > 2) throw new Error(`chunk chain too deep: ${entry}`);\n}\n\u002F\u002F trade-off: depth is a proxy — two deep-but-tiny hops may be fine while one\n\u002F\u002F huge chunk is not. Pair it with a per-chunk size budget rather than relying\n\u002F\u002F on either signal alone.\n",[30,918,919,924,940,976,1007,1026,1035,1053,1111,1116,1134,1175,1179,1184,1190],{"__ignoreMap":281},[285,920,921],{"class":176,"line":287},[285,922,923],{"class":290},"\u002F\u002F Fail the build if any route needs more than two sequential chunk fetches.\n",[285,925,926,929,932,935,938],{"class":176,"line":294},[285,927,928],{"class":297},"import",[285,930,931],{"class":311}," { readFileSync } ",[285,933,934],{"class":297},"from",[285,936,937],{"class":345}," 'node:fs'",[285,939,410],{"class":311},[285,941,942,944,947,949,952,954,957,959,962,964,967,970,973],{"class":176,"line":308},[285,943,455],{"class":297},[285,945,946],{"class":370}," graph",[285,948,602],{"class":297},[285,950,951],{"class":370}," JSON",[285,953,523],{"class":311},[285,955,956],{"class":318},"parse",[285,958,322],{"class":311},[285,960,961],{"class":318},"readFileSync",[285,963,322],{"class":311},[285,965,966],{"class":345},"'dist\u002Fstats.json'",[285,968,969],{"class":311},", ",[285,971,972],{"class":345},"'utf8'",[285,974,975],{"class":311},"));\n",[285,977,978,980,983,985,987,989,991,994,996,998,1000,1003,1005],{"class":176,"line":315},[285,979,455],{"class":297},[285,981,982],{"class":318}," depth",[285,984,602],{"class":297},[285,986,788],{"class":311},[285,988,325],{"class":304},[285,990,969],{"class":311},[285,992,993],{"class":304},"seen",[285,995,602],{"class":297},[285,997,724],{"class":297},[285,999,727],{"class":318},[285,1001,1002],{"class":311},"()) ",[285,1004,754],{"class":297},[285,1006,305],{"class":311},[285,1008,1009,1011,1014,1016,1019,1021,1024],{"class":176,"line":331},[285,1010,785],{"class":297},[285,1012,1013],{"class":311}," (seen.",[285,1015,803],{"class":318},[285,1017,1018],{"class":311},"(id)) ",[285,1020,352],{"class":297},[285,1022,1023],{"class":370}," 0",[285,1025,410],{"class":311},[285,1027,1028,1030,1032],{"class":176,"line":364},[285,1029,815],{"class":311},[285,1031,818],{"class":318},[285,1033,1034],{"class":311},"(id);\n",[285,1036,1037,1039,1042,1044,1047,1050],{"class":176,"line":380},[285,1038,653],{"class":297},[285,1040,1041],{"class":370}," kids",[285,1043,602],{"class":297},[285,1045,1046],{"class":311}," graph.chunks[id]?.dynamicImports ",[285,1048,1049],{"class":297},"??",[285,1051,1052],{"class":311}," [];\n",[285,1054,1055,1058,1061,1063,1066,1069,1072,1075,1078,1080,1083,1086,1089,1092,1095,1097,1099,1101,1104,1107,1109],{"class":176,"line":386},[285,1056,1057],{"class":297},"  return",[285,1059,1060],{"class":311}," kids.",[285,1062,646],{"class":370},[285,1064,1065],{"class":297}," ?",[285,1067,1068],{"class":370}," 1",[285,1070,1071],{"class":297}," +",[285,1073,1074],{"class":311}," Math.",[285,1076,1077],{"class":318},"max",[285,1079,322],{"class":311},[285,1081,1082],{"class":297},"...",[285,1084,1085],{"class":311},"kids.",[285,1087,1088],{"class":318},"map",[285,1090,1091],{"class":311},"((",[285,1093,1094],{"class":304},"k",[285,1096,751],{"class":311},[285,1098,754],{"class":297},[285,1100,982],{"class":318},[285,1102,1103],{"class":311},"(k, seen))) ",[285,1105,1106],{"class":297},":",[285,1108,1023],{"class":370},[285,1110,410],{"class":311},[285,1112,1113],{"class":176,"line":398},[285,1114,1115],{"class":311},"};\n",[285,1117,1118,1121,1123,1125,1128,1131],{"class":176,"line":404},[285,1119,1120],{"class":297},"for",[285,1122,788],{"class":311},[285,1124,455],{"class":297},[285,1126,1127],{"class":370}," entry",[285,1129,1130],{"class":297}," of",[285,1132,1133],{"class":311}," graph.entrypoints) {\n",[285,1135,1136,1138,1140,1143,1146,1149,1152,1154,1157,1159,1162,1164,1167,1170,1173],{"class":176,"line":413},[285,1137,785],{"class":297},[285,1139,788],{"class":311},[285,1141,1142],{"class":318},"depth",[285,1144,1145],{"class":311},"(entry) ",[285,1147,1148],{"class":297},">",[285,1150,1151],{"class":370}," 2",[285,1153,751],{"class":311},[285,1155,1156],{"class":297},"throw",[285,1158,724],{"class":297},[285,1160,1161],{"class":318}," Error",[285,1163,322],{"class":311},[285,1165,1166],{"class":345},"`chunk chain too deep: ${",[285,1168,1169],{"class":311},"entry",[285,1171,1172],{"class":345},"}`",[285,1174,780],{"class":311},[285,1176,1177],{"class":176,"line":419},[285,1178,682],{"class":311},[285,1180,1181],{"class":176,"line":425},[285,1182,1183],{"class":290},"\u002F\u002F trade-off: depth is a proxy — two deep-but-tiny hops may be fine while one\n",[285,1185,1187],{"class":176,"line":1186},14,[285,1188,1189],{"class":290},"\u002F\u002F huge chunk is not. Pair it with a per-chunk size budget rather than relying\n",[285,1191,1193],{"class":176,"line":1192},15,[285,1194,1195],{"class":290},"\u002F\u002F on either signal alone.\n",[15,1197,1198],{},"In the field, watch the route's interaction-to-render time rather than initial LCP: waterfall damage from nested imports lands on navigation, which page-load metrics do not capture at all.",[51,1200,1202],{"id":1201},"faq","FAQ",[1204,1205,1208,1212],"details",{"className":1206},[1207],"faq-item",[1209,1210,1211],"summary",{},"Is splitting into more chunks always better for caching?",[15,1213,1214],{},"No. Finer chunks improve cache reuse when only one part changes, but each additional chunk on the critical path costs a request and, if it is discovered late, a round trip. The balance depends on how often each part changes: split along genuine change boundaries — vendor versus app, rarely versus frequently updated — not by module count.",[1204,1216,1218,1221],{"className":1217},[1207],[1209,1219,1220],{},"Does HTTP\u002F2 multiplexing make waterfalls harmless?",[15,1222,1223,1224,1227],{},"It removes the connection cost of extra requests, not the dependency. A chunk that cannot be ",[39,1225,1226],{},"discovered"," until its parent executes still waits a full round trip, no matter how many requests share the connection. Multiplexing makes parallel fetching cheap; it does nothing for sequential discovery, which is what nesting creates.",[1204,1229,1231,1234],{"className":1230},[1207],[1209,1232,1233],{},"Should I inline tiny chunks entirely?",[15,1235,1236,1237,1239],{},"Usually yes, below roughly ",[30,1238,249],{}," on the critical path. The break-even is a comparison between the transfer saving and the round-trip cost, and on a mobile connection a round trip is worth far more than a few kilobytes. Keep small modules separate only when they are genuinely conditional and rarely needed.",[1204,1241,1243,1246],{"className":1242},[1207],[1209,1244,1245],{},"How deep is too deep for a chunk chain?",[15,1247,1248],{},"Two levels is the practical ceiling on a mobile connection: the entry plus one dependent fetch. Each additional level adds a full round trip that no amount of bandwidth removes, because the request cannot be issued until the previous chunk has executed. If a route genuinely needs three tiers of code, hoist the middle one into the entry or preload it — the byte cost of a slightly larger parent is almost always cheaper than the latency of another hop.",[1204,1250,1252,1255],{"className":1251},[1207],[1209,1253,1254],{},"Does server-side rendering hide this problem?",[15,1256,1257,1258,1261],{},"It hides the ",[39,1259,1260],{},"paint"," symptom and keeps the interaction one. Server rendering means the route's HTML arrives without waiting for any chunk, so the page looks complete while the chain is still resolving — and the controls in it stay inert until the last chunk lands. That is arguably worse for the user than a visible loading state, because the page invites interaction it cannot yet honour. Measure interaction readiness, not first paint, on server-rendered routes.",[1204,1263,1265,1268],{"className":1264},[1207],[1209,1266,1267],{},"Should I prefetch every route chunk on page load?",[15,1269,1270],{},"No. Prefetching consumes bandwidth that the current page still needs, and on a mobile connection a background chunk download measurably delays the LCP resource. Prefetch on intent — pointer entry, focus, or a link scrolling into view — and skip it entirely when the connection reports reduced-data mode. That gets you most of the perceived benefit with none of the contention.",[51,1272,1274],{"id":1273},"related","Related",[56,1276,1277,1282,1289],{},[59,1278,1279,1281],{},[19,1280,22],{"href":21}," — choosing the split boundaries in the first place.",[59,1283,1284,1288],{},[19,1285,1287],{"href":1286},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fvite-vs-webpack-bundle-splitting-performance\u002F","Vite vs Webpack bundle splitting performance"," — how each bundler groups modules by default.",[59,1290,1291,1295],{},[19,1292,1294],{"href":1293},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002F","Resource Hints & Early Hints"," — starting the first chunk before the parser reaches it.",[1297,1298,1300],"script",{"type":1299},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Fix waterfalls from nested dynamic imports\",\n  \"description\": \"Detect serialised chunk chains, merge chunks too small to justify a round trip, hoist and parallelise nested imports, preload the next hop, and assert chain depth in CI.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Merge tiny chunks\", \"text\": \"Fold modules below roughly twenty kilobytes into their parent so they do not cost a round trip.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Parallelise the imports\", \"text\": \"Hoist a nested import to the parent and fetch both with Promise.all so discovery is not serialised.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Warm the next hop\", \"text\": \"Start a conditional import without awaiting it so it is in flight by the time it is needed.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Assert chain depth\", \"text\": \"Fail the build when any entry point needs more than two sequential chunk fetches.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F#verification\" }\n  ]\n}\n",[1297,1302,1303],{"type":1299},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Fix Request Waterfalls Caused by Nested Dynamic Imports\",\n  \"description\": \"Code splitting trades bytes for round trips. Find the serialised chunk chain and collapse it without losing the byte saving.\",\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\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F\" }\n}\n",[1297,1305,1306],{"type":1299},"\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\": \"JavaScript Bundle Optimization & Code Splitting\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Dynamic Imports and Route-Based Splitting\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Fixing Waterfalls from Nested Dynamic Imports\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F\" }\n  ]\n}\n",[1308,1309,1310],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}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}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}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":281,"searchDepth":294,"depth":294,"links":1312},[1313,1314,1315,1316,1317,1318],{"id":53,"depth":294,"text":54},{"id":230,"depth":294,"text":231},{"id":268,"depth":294,"text":269},{"id":906,"depth":294,"text":907},{"id":1201,"depth":294,"text":1202},{"id":1273,"depth":294,"text":1274},"Code splitting can trade bytes for round trips. Detect the serialised chunk chain and collapse it without giving up the split.","md",{"slug":1322,"type":1323,"breadcrumb":1324,"datePublished":1332,"dateModified":1332},"fixing-waterfalls-from-nested-dynamic-imports","article",[1325,1328,1329,1330],{"name":1326,"url":1327},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1331},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F","2026-08-01","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports",{"title":1335,"description":1336},"Fixing Request Waterfalls from Nested Dynamic Imports","A route chunk that imports another chunk that imports a third serialises three round trips. Flatten the graph, preload the next hop, and prefetch on intent.","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002Findex","bDMxU-SfPMo6OQ2BEussv7IIBp6rNRGP_v9gfDVgA0M",[1340,1343],{"title":22,"path":1341,"stem":1342,"children":-1},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Findex",{"title":1344,"path":1345,"stem":1346,"children":-1},"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",1785622406638]