[{"data":1,"prerenderedAt":1484},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport":1475},{"id":4,"title":5,"body":6,"description":1456,"extension":1457,"meta":1458,"navigation":699,"path":1470,"seo":1471,"stem":1473,"__hash__":1474},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport\u002Findex.md","Prefetching Route Chunks on Hover and Viewport",{"type":7,"value":8,"toc":1440},"minimark",[9,14,29,37,193,198,239,243,249,255,261,267,404,408,413,619,622,626,894,899,903,914,1158,1161,1165,1168,1286,1290,1298,1302,1305,1309,1342,1346,1349,1353,1369,1385,1398,1402,1425,1430,1433,1436],[10,11,13],"h1",{"id":12},"how-to-prefetch-route-chunks-on-hover-and-viewport","How to Prefetch Route Chunks on Hover and Viewport",[15,16,17,18,23,24,28],"p",{},"This guide builds on ",[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",". Route-based splitting shrinks the initial bundle by moving each route's code into its own chunk — and in exchange, every first visit to a route waits for that chunk to download, parse and evaluate. On a mobile connection that is commonly 200–600ms added to the transition, enough to make a fast SPA feel slower than a server-rendered site.",[15,30,31,32,36],{},"Prefetching closes the gap by fetching the chunk before the user clicks. The art is in the trigger: prefetch too eagerly (every link on the page at load) and you waste bandwidth and compete with the current page's resources; prefetch too late (on click) and you gain nothing. The useful triggers are viewport visibility for likely next steps, hover or focus intent for desktop users, and ",[33,34,35],"code",{},"pointerdown"," as a last-moment head start on touch devices.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,71,47,77,47,82,47,92,47,99,47,105,47,109,47,114,47,118,47,122,47,125,47,129,47,132,47,135,47,142,47,148,47,152,47,156,47,160,47,164,47,168,47,172,47,176,47,180,47,184,47,189,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 222","100%","img","Two timelines of a navigation to a code-split route, comparing loading the chunk on click with prefetching it on hover.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Route transition with and without prefetch",[60,61,45],"desc",{},[49,63],{"x":64,"y":64,"width":65,"height":66,"rx":67,"fill":68,"stroke":69,"style":70},"1","758","220","10","none","currentColor","stroke-opacity:0.18",[72,73,58],"text",{"x":74,"y":75,"fill":69,"style":76},"28.0","34.0","font-size:16px;font-weight:700",[72,78,81],{"x":74,"y":79,"fill":69,"style":80},"74.0","font-size:12.5px;font-weight:700","On click",[49,83],{"x":84,"y":85,"width":86,"height":87,"rx":88,"fill":89,"stroke":90,"style":91},"139.1","56.0","246.6","26.0","3","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[72,93,98],{"x":94,"y":95,"fill":69,"style":96,"textAnchor":97},"262.4","73.5","font-size:11.5px","middle","chunk download",[49,100],{"x":101,"y":85,"width":102,"height":87,"rx":88,"fill":103,"stroke":103,"style":104},"387.2","57.3","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[72,106,108],{"x":107,"y":95,"fill":69,"style":96,"textAnchor":97},"415.9","eval",[49,110],{"x":111,"y":85,"width":112,"height":87,"rx":88,"fill":69,"stroke":69,"style":113},"446.0","116.0","fill-opacity:0.06;stroke-opacity:0.4",[72,115,117],{"x":116,"y":95,"fill":69,"style":96,"textAnchor":97},"504.0","render",[72,119,121],{"x":74,"y":120,"fill":69,"style":80},"112.0","Hover prefetch",[49,123],{"x":84,"y":124,"width":102,"height":87,"rx":88,"fill":103,"stroke":103,"style":104},"94.0",[72,126,108],{"x":127,"y":128,"fill":69,"style":96,"textAnchor":97},"167.8","111.5",[49,130],{"x":131,"y":124,"width":112,"height":87,"rx":88,"fill":69,"stroke":69,"style":113},"197.9",[72,133,117],{"x":134,"y":128,"fill":69,"style":96,"textAnchor":97},"255.9",[136,137],"line",{"x1":138,"y1":139,"x2":140,"y2":139,"stroke":69,"style":141},"138.4","134.0","726.0","stroke-opacity:0.4",[72,143,147],{"x":144,"y":145,"fill":146,"style":96,"textAnchor":97},"150.4","151.0","#51617a","0ms",[72,149,151],{"x":150,"y":145,"fill":146,"style":96,"textAnchor":97},"203.7","100ms",[72,153,155],{"x":154,"y":145,"fill":146,"style":96,"textAnchor":97},"269.0","200ms",[72,157,159],{"x":158,"y":145,"fill":146,"style":96,"textAnchor":97},"334.2","300ms",[72,161,163],{"x":162,"y":145,"fill":146,"style":96,"textAnchor":97},"399.5","400ms",[72,165,167],{"x":166,"y":145,"fill":146,"style":96,"textAnchor":97},"464.8","500ms",[72,169,171],{"x":170,"y":145,"fill":146,"style":96,"textAnchor":97},"530.1","600ms",[72,173,175],{"x":174,"y":145,"fill":146,"style":96,"textAnchor":97},"595.4","700ms",[72,177,179],{"x":178,"y":145,"fill":146,"style":96,"textAnchor":97},"660.7","800ms",[72,181,183],{"x":182,"y":145,"fill":146,"style":96,"textAnchor":97},"713.3","900ms",[72,185,188],{"x":74,"y":186,"fill":146,"style":187},"182.0","font-size:12.5px","With hover intent the chunk was already downloaded during the ~300ms between hover and click; only evaluation",[72,190,192],{"x":74,"y":191,"fill":146,"style":187},"200.0","and render remain.",[194,195,197],"h2",{"id":196},"rapid-diagnosis","Rapid Diagnosis",[199,200,201,209,223,229],"ul",{},[202,203,204,208],"li",{},[205,206,207],"strong",{},"Measure transition time for first visits to a route."," If it is dominated by a chunk request that starts on click, prefetching will help.",[202,210,211,214,215,218,219,222],{},[205,212,213],{},"Check what your framework already does."," Next.js ",[33,216,217],{},"\u003CLink>",", Nuxt ",[33,220,221],{},"\u003CNuxtLink>",", SvelteKit and Remix all have built-in prefetch behaviour; custom routers usually have none.",[202,224,225,228],{},[205,226,227],{},"Look at bandwidth contention."," If prefetches fire during the initial load, they can delay the current page's LCP resources.",[202,230,231,234,235,238],{},[205,232,233],{},"Check data saver and connection type."," Prefetching on ",[33,236,237],{},"saveData"," or 2G connections spends users' data on guesses.",[194,240,242],{"id":241},"root-cause-analysis","Root Cause Analysis",[15,244,245,248],{},[205,246,247],{},"1. Chunks fetched only on demand."," Lazy routes trade initial size for transition latency; without prefetch, the trade is paid in full on every first visit.",[15,250,251,254],{},[205,252,253],{},"2. Over-eager prefetching."," Prefetching every link at load fills the network queue with low-value requests during the page's most critical seconds.",[15,256,257,260],{},[205,258,259],{},"3. Prefetch without the dependency graph."," Prefetching only the route's entry chunk leaves its imported chunks to load after the click — a smaller waterfall, but still a waterfall.",[15,262,263,266],{},[205,264,265],{},"4. Data not prefetched."," On data-heavy routes, the API call dominates the transition; prefetching only code leaves most of the latency in place.",[15,268,269],{},[40,270,47,273,47,276,47,279,47,281,47,284,47,286,47,290,47,296,47,300,47,304,47,307,47,311,47,314,47,318,47,322,47,326,47,328,47,332,47,334,47,337,47,339,47,342,47,344,47,348,47,350,47,353,47,355,47,358,47,360,47,363,47,366,47,370,47,372,47,375,47,377,47,380,47,382,47,384,47,387,47,391,47,393,47,395,47,397,47,400,47,402,47],{"viewBox":271,"width":43,"role":44,"ariaLabel":272,"style":46},"0 0 760 228","Comparison of prefetch triggers by head start before click, wasted fetches and suitability for touch devices.",[49,274],{"className":275,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,277,278],{},"Prefetch triggers compared",[60,280,272],{},[49,282],{"x":64,"y":64,"width":65,"height":283,"rx":67,"fill":68,"stroke":69,"style":70},"226",[72,285,278],{"x":74,"y":75,"fill":69,"style":76},[49,287],{"x":74,"y":85,"width":288,"height":289,"rx":53,"fill":69,"stroke":69,"style":113},"180.7","30.0",[72,291,295],{"x":292,"y":293,"fill":69,"style":80,"textAnchor":294},"38.0","75.5","start","Trigger",[49,297],{"x":298,"y":85,"width":299,"height":289,"rx":53,"fill":69,"stroke":69,"style":113},"208.7","174.4",[72,301,303],{"x":302,"y":293,"fill":69,"style":80,"textAnchor":97},"295.9","Head start",[49,305],{"x":306,"y":85,"width":299,"height":289,"rx":53,"fill":69,"stroke":69,"style":113},"383.1",[72,308,310],{"x":309,"y":293,"fill":69,"style":80,"textAnchor":97},"470.3","Waste",[49,312],{"x":313,"y":85,"width":299,"height":289,"rx":53,"fill":69,"stroke":69,"style":113},"557.6",[72,315,317],{"x":316,"y":293,"fill":69,"style":80,"textAnchor":97},"644.8","Touch devices",[49,319],{"x":74,"y":320,"width":288,"height":289,"rx":53,"fill":68,"stroke":69,"style":321},"86.0","stroke-opacity:0.35",[72,323,325],{"x":292,"y":324,"fill":69,"style":80,"textAnchor":294},"105.5","Viewport visible",[49,327],{"x":298,"y":320,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,329,331],{"x":302,"y":324,"fill":69,"style":330,"textAnchor":97},"font-size:12px","seconds",[49,333],{"x":306,"y":320,"width":299,"height":289,"rx":53,"fill":89,"stroke":90,"style":91},[72,335,336],{"x":309,"y":324,"fill":69,"style":330,"textAnchor":97},"high",[49,338],{"x":313,"y":320,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,340,341],{"x":316,"y":324,"fill":69,"style":330,"textAnchor":97},"works",[49,343],{"x":74,"y":112,"width":288,"height":289,"rx":53,"fill":68,"stroke":69,"style":321},[72,345,347],{"x":292,"y":346,"fill":69,"style":80,"textAnchor":294},"135.5","Hover \u002F focus (with delay)",[49,349],{"x":298,"y":112,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,351,352],{"x":302,"y":346,"fill":69,"style":330,"textAnchor":97},"100-400ms",[49,354],{"x":306,"y":112,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,356,357],{"x":309,"y":346,"fill":69,"style":330,"textAnchor":97},"low",[49,359],{"x":313,"y":112,"width":299,"height":289,"rx":53,"fill":89,"stroke":90,"style":91},[72,361,362],{"x":316,"y":346,"fill":69,"style":330,"textAnchor":97},"no hover",[49,364],{"x":74,"y":365,"width":288,"height":289,"rx":53,"fill":68,"stroke":69,"style":321},"146.0",[72,367,369],{"x":292,"y":368,"fill":69,"style":80,"textAnchor":294},"165.5","pointerdown \u002F touchstart",[49,371],{"x":298,"y":365,"width":299,"height":289,"rx":53,"fill":69,"stroke":69,"style":113},[72,373,374],{"x":302,"y":368,"fill":69,"style":330,"textAnchor":97},"50-150ms",[49,376],{"x":306,"y":365,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,378,379],{"x":309,"y":368,"fill":69,"style":330,"textAnchor":97},"very low",[49,381],{"x":313,"y":365,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,383,341],{"x":316,"y":368,"fill":69,"style":330,"textAnchor":97},[49,385],{"x":74,"y":386,"width":288,"height":289,"rx":53,"fill":68,"stroke":69,"style":321},"176.0",[72,388,390],{"x":292,"y":389,"fill":69,"style":80,"textAnchor":294},"195.5","Idle after load (likely routes)",[49,392],{"x":298,"y":386,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,394,331],{"x":302,"y":389,"fill":69,"style":330,"textAnchor":97},[49,396],{"x":306,"y":386,"width":299,"height":289,"rx":53,"fill":69,"stroke":69,"style":113},[72,398,399],{"x":309,"y":389,"fill":69,"style":330,"textAnchor":97},"medium",[49,401],{"x":313,"y":386,"width":299,"height":289,"rx":53,"fill":103,"stroke":103,"style":104},[72,403,341],{"x":316,"y":389,"fill":69,"style":330,"textAnchor":97},[194,405,407],{"id":406},"step-by-step-resolution","Step-by-Step Resolution",[409,410,412],"h3",{"id":411},"_1-build-a-single-prefetch-function-with-network-guards","1. Build a single prefetch function with network guards",[414,415,420],"pre",{"className":416,"code":417,"language":418,"meta":419,"style":419},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F prefetch.js\nconst done = new Set();\nexport function prefetchRoute(load) {\n  if (done.has(load)) return;\n  const c = navigator.connection;\n  if (c?.saveData || \u002F2g\u002F.test(c?.effectiveType ?? '')) return;   \u002F\u002F respect data saver\n  done.add(load);\n  load().catch(() => done.delete(load));                         \u002F\u002F retry next time if it failed\n}\n\u002F\u002F trade-off: calling the route's import() function triggers the bundler's\n\u002F\u002F preload of its dependency graph at normal priority. For viewport prefetching\n\u002F\u002F of many links, use \u003Clink rel=\"prefetch\"> (idle priority) instead — step 3.\n","javascript","",[33,421,422,430,455,477,498,512,553,565,595,601,607,613],{"__ignoreMap":419},[423,424,426],"span",{"class":136,"line":425},1,[423,427,429],{"class":428},"sjfSM","\u002F\u002F prefetch.js\n",[423,431,433,437,441,444,447,451],{"class":136,"line":432},2,[423,434,436],{"class":435},"sPARh","const",[423,438,440],{"class":439},"sPXB4"," done",[423,442,443],{"class":435}," =",[423,445,446],{"class":435}," new",[423,448,450],{"class":449},"smZ65"," Set",[423,452,454],{"class":453},"saISM","();\n",[423,456,458,461,464,467,470,474],{"class":136,"line":457},3,[423,459,460],{"class":435},"export",[423,462,463],{"class":435}," function",[423,465,466],{"class":449}," prefetchRoute",[423,468,469],{"class":453},"(",[423,471,473],{"class":472},"sQw3B","load",[423,475,476],{"class":453},") {\n",[423,478,480,483,486,489,492,495],{"class":136,"line":479},4,[423,481,482],{"class":435},"  if",[423,484,485],{"class":453}," (done.",[423,487,488],{"class":449},"has",[423,490,491],{"class":453},"(load)) ",[423,493,494],{"class":435},"return",[423,496,497],{"class":453},";\n",[423,499,501,504,507,509],{"class":136,"line":500},5,[423,502,503],{"class":435},"  const",[423,505,506],{"class":439}," c",[423,508,443],{"class":435},[423,510,511],{"class":453}," navigator.connection;\n",[423,513,515,517,520,523,527,530,533,536,539,542,545,547,550],{"class":136,"line":514},6,[423,516,482],{"class":435},[423,518,519],{"class":453}," (c?.saveData ",[423,521,522],{"class":435},"||",[423,524,526],{"class":525},"sZ8jY"," \u002F2g\u002F",[423,528,529],{"class":453},".",[423,531,532],{"class":449},"test",[423,534,535],{"class":453},"(c?.effectiveType ",[423,537,538],{"class":435},"??",[423,540,541],{"class":525}," ''",[423,543,544],{"class":453},")) ",[423,546,494],{"class":435},[423,548,549],{"class":453},";   ",[423,551,552],{"class":428},"\u002F\u002F respect data saver\n",[423,554,556,559,562],{"class":136,"line":555},7,[423,557,558],{"class":453},"  done.",[423,560,561],{"class":449},"add",[423,563,564],{"class":453},"(load);\n",[423,566,568,571,574,577,580,583,586,589,592],{"class":136,"line":567},8,[423,569,570],{"class":449},"  load",[423,572,573],{"class":453},"().",[423,575,576],{"class":449},"catch",[423,578,579],{"class":453},"(() ",[423,581,582],{"class":435},"=>",[423,584,585],{"class":453}," done.",[423,587,588],{"class":449},"delete",[423,590,591],{"class":453},"(load));                         ",[423,593,594],{"class":428},"\u002F\u002F retry next time if it failed\n",[423,596,598],{"class":136,"line":597},9,[423,599,600],{"class":453},"}\n",[423,602,604],{"class":136,"line":603},10,[423,605,606],{"class":428},"\u002F\u002F trade-off: calling the route's import() function triggers the bundler's\n",[423,608,610],{"class":136,"line":609},11,[423,611,612],{"class":428},"\u002F\u002F preload of its dependency graph at normal priority. For viewport prefetching\n",[423,614,616],{"class":136,"line":615},12,[423,617,618],{"class":428},"\u002F\u002F of many links, use \u003Clink rel=\"prefetch\"> (idle priority) instead — step 3.\n",[15,620,621],{},"Expected outcome: one entry point for all prefetching, with consistent guards.",[409,623,625],{"id":624},"_2-prefetch-on-intent-hover-focus-and-pointerdown","2. Prefetch on intent: hover, focus and pointerdown",[414,627,629],{"className":416,"code":628,"language":418,"meta":419,"style":419},"import { prefetchRoute } from '.\u002Fprefetch.js';\nconst routes = { '\u002Fpricing': () => import('.\u002Fpages\u002FPricing.jsx'), '\u002Fdocs': () => import('.\u002Fpages\u002FDocs.jsx') };\n\nfunction onIntent(e) {\n  const a = e.target.closest('a[href]');\n  const load = a && routes[new URL(a.href).pathname];\n  if (load) prefetchRoute(load);\n}\nlet hoverTimer;\ndocument.addEventListener('mouseover', (e) => { clearTimeout(hoverTimer); hoverTimer = setTimeout(() => onIntent(e), 65); });\ndocument.addEventListener('focusin', onIntent);\ndocument.addEventListener('pointerdown', onIntent, { passive: true });\n\u002F\u002F trade-off: a 65ms hover delay filters out cursor fly-overs but costs a little\n\u002F\u002F head start. Raise it on pages with dense link lists (mega-menus) to avoid\n\u002F\u002F prefetching every item the cursor crosses.\n",[33,630,631,647,695,701,716,739,766,778,782,790,842,856,876,882,888],{"__ignoreMap":419},[423,632,633,636,639,642,645],{"class":136,"line":425},[423,634,635],{"class":435},"import",[423,637,638],{"class":453}," { prefetchRoute } ",[423,640,641],{"class":435},"from",[423,643,644],{"class":525}," '.\u002Fprefetch.js'",[423,646,497],{"class":453},[423,648,649,651,654,656,659,662,665,667,670,672,675,678,681,683,685,687,689,692],{"class":136,"line":432},[423,650,436],{"class":435},[423,652,653],{"class":439}," routes",[423,655,443],{"class":435},[423,657,658],{"class":453}," { ",[423,660,661],{"class":525},"'\u002Fpricing'",[423,663,664],{"class":453},": () ",[423,666,582],{"class":435},[423,668,669],{"class":435}," import",[423,671,469],{"class":453},[423,673,674],{"class":525},"'.\u002Fpages\u002FPricing.jsx'",[423,676,677],{"class":453},"), ",[423,679,680],{"class":525},"'\u002Fdocs'",[423,682,664],{"class":453},[423,684,582],{"class":435},[423,686,669],{"class":435},[423,688,469],{"class":453},[423,690,691],{"class":525},"'.\u002Fpages\u002FDocs.jsx'",[423,693,694],{"class":453},") };\n",[423,696,697],{"class":136,"line":457},[423,698,700],{"emptyLinePlaceholder":699},true,"\n",[423,702,703,706,709,711,714],{"class":136,"line":479},[423,704,705],{"class":435},"function",[423,707,708],{"class":449}," onIntent",[423,710,469],{"class":453},[423,712,713],{"class":472},"e",[423,715,476],{"class":453},[423,717,718,720,723,725,728,731,733,736],{"class":136,"line":500},[423,719,503],{"class":435},[423,721,722],{"class":439}," a",[423,724,443],{"class":435},[423,726,727],{"class":453}," e.target.",[423,729,730],{"class":449},"closest",[423,732,469],{"class":453},[423,734,735],{"class":525},"'a[href]'",[423,737,738],{"class":453},");\n",[423,740,741,743,746,748,751,754,757,760,763],{"class":136,"line":514},[423,742,503],{"class":435},[423,744,745],{"class":439}," load",[423,747,443],{"class":435},[423,749,750],{"class":453}," a ",[423,752,753],{"class":435},"&&",[423,755,756],{"class":453}," routes[",[423,758,759],{"class":435},"new",[423,761,762],{"class":449}," URL",[423,764,765],{"class":453},"(a.href).pathname];\n",[423,767,768,770,773,776],{"class":136,"line":555},[423,769,482],{"class":435},[423,771,772],{"class":453}," (load) ",[423,774,775],{"class":449},"prefetchRoute",[423,777,564],{"class":453},[423,779,780],{"class":136,"line":567},[423,781,600],{"class":453},[423,783,784,787],{"class":136,"line":597},[423,785,786],{"class":435},"let",[423,788,789],{"class":453}," hoverTimer;\n",[423,791,792,795,798,800,803,806,808,811,813,815,818,821,824,827,829,831,833,836,839],{"class":136,"line":603},[423,793,794],{"class":453},"document.",[423,796,797],{"class":449},"addEventListener",[423,799,469],{"class":453},[423,801,802],{"class":525},"'mouseover'",[423,804,805],{"class":453},", (",[423,807,713],{"class":472},[423,809,810],{"class":453},") ",[423,812,582],{"class":435},[423,814,658],{"class":453},[423,816,817],{"class":449},"clearTimeout",[423,819,820],{"class":453},"(hoverTimer); hoverTimer ",[423,822,823],{"class":435},"=",[423,825,826],{"class":449}," setTimeout",[423,828,579],{"class":453},[423,830,582],{"class":435},[423,832,708],{"class":449},[423,834,835],{"class":453},"(e), ",[423,837,838],{"class":439},"65",[423,840,841],{"class":453},"); });\n",[423,843,844,846,848,850,853],{"class":136,"line":609},[423,845,794],{"class":453},[423,847,797],{"class":449},[423,849,469],{"class":453},[423,851,852],{"class":525},"'focusin'",[423,854,855],{"class":453},", onIntent);\n",[423,857,858,860,862,864,867,870,873],{"class":136,"line":615},[423,859,794],{"class":453},[423,861,797],{"class":449},[423,863,469],{"class":453},[423,865,866],{"class":525},"'pointerdown'",[423,868,869],{"class":453},", onIntent, { passive: ",[423,871,872],{"class":439},"true",[423,874,875],{"class":453}," });\n",[423,877,879],{"class":136,"line":878},13,[423,880,881],{"class":428},"\u002F\u002F trade-off: a 65ms hover delay filters out cursor fly-overs but costs a little\n",[423,883,885],{"class":136,"line":884},14,[423,886,887],{"class":428},"\u002F\u002F head start. Raise it on pages with dense link lists (mega-menus) to avoid\n",[423,889,891],{"class":136,"line":890},15,[423,892,893],{"class":428},"\u002F\u002F prefetching every item the cursor crosses.\n",[15,895,896,897,529],{},"Expected outcome: desktop users typically gain 150–400ms of head start; touch users get a smaller but free head start from ",[33,898,35],{},[409,900,902],{"id":901},"_3-prefetch-likely-next-routes-in-the-viewport-at-idle-priority","3. Prefetch likely next routes in the viewport at idle priority",[15,904,905,906,909,910,913],{},"For primary calls to action visible on the page, use ",[33,907,908],{},"IntersectionObserver"," and low-priority ",[33,911,912],{},"\u003Clink rel=\"prefetch\">"," for the route's files from the build manifest.",[414,915,917],{"className":416,"code":916,"language":418,"meta":419,"style":419},"const io = new IntersectionObserver((entries) => {\n  for (const { isIntersecting, target } of entries) {\n    if (!isIntersecting) continue;\n    io.unobserve(target);\n    requestIdleCallback(() => {\n      for (const href of routeFiles(target.pathname)) {        \u002F\u002F from the Vite\u002Fwebpack manifest\n        const l = document.createElement('link'); l.rel = 'prefetch'; l.href = href; document.head.append(l);\n      }\n    });\n  }\n}, { rootMargin: '200px' });\ndocument.querySelectorAll('a[data-prefetch]').forEach((a) => io.observe(a));\n\u002F\u002F trade-off: limit viewport prefetching to links you mark explicitly. On a\n\u002F\u002F listing page with 60 product links, prefetching all of them wastes far more\n\u002F\u002F than it saves — prefetch the product route chunk once, not per link.\n",[33,918,919,946,976,994,1005,1016,1040,1083,1088,1093,1098,1108,1143,1148,1153],{"__ignoreMap":419},[423,920,921,923,926,928,930,933,936,939,941,943],{"class":136,"line":425},[423,922,436],{"class":435},[423,924,925],{"class":439}," io",[423,927,443],{"class":435},[423,929,446],{"class":435},[423,931,932],{"class":449}," IntersectionObserver",[423,934,935],{"class":453},"((",[423,937,938],{"class":472},"entries",[423,940,810],{"class":453},[423,942,582],{"class":435},[423,944,945],{"class":453}," {\n",[423,947,948,951,954,956,958,961,964,967,970,973],{"class":136,"line":432},[423,949,950],{"class":435},"  for",[423,952,953],{"class":453}," (",[423,955,436],{"class":435},[423,957,658],{"class":453},[423,959,960],{"class":439},"isIntersecting",[423,962,963],{"class":453},", ",[423,965,966],{"class":439},"target",[423,968,969],{"class":453}," } ",[423,971,972],{"class":435},"of",[423,974,975],{"class":453}," entries) {\n",[423,977,978,981,983,986,989,992],{"class":136,"line":457},[423,979,980],{"class":435},"    if",[423,982,953],{"class":453},[423,984,985],{"class":435},"!",[423,987,988],{"class":453},"isIntersecting) ",[423,990,991],{"class":435},"continue",[423,993,497],{"class":453},[423,995,996,999,1002],{"class":136,"line":479},[423,997,998],{"class":453},"    io.",[423,1000,1001],{"class":449},"unobserve",[423,1003,1004],{"class":453},"(target);\n",[423,1006,1007,1010,1012,1014],{"class":136,"line":500},[423,1008,1009],{"class":449},"    requestIdleCallback",[423,1011,579],{"class":453},[423,1013,582],{"class":435},[423,1015,945],{"class":453},[423,1017,1018,1021,1023,1025,1028,1031,1034,1037],{"class":136,"line":514},[423,1019,1020],{"class":435},"      for",[423,1022,953],{"class":453},[423,1024,436],{"class":435},[423,1026,1027],{"class":439}," href",[423,1029,1030],{"class":435}," of",[423,1032,1033],{"class":449}," routeFiles",[423,1035,1036],{"class":453},"(target.pathname)) {        ",[423,1038,1039],{"class":428},"\u002F\u002F from the Vite\u002Fwebpack manifest\n",[423,1041,1042,1045,1048,1050,1053,1056,1058,1061,1064,1066,1069,1072,1074,1077,1080],{"class":136,"line":555},[423,1043,1044],{"class":435},"        const",[423,1046,1047],{"class":439}," l",[423,1049,443],{"class":435},[423,1051,1052],{"class":453}," document.",[423,1054,1055],{"class":449},"createElement",[423,1057,469],{"class":453},[423,1059,1060],{"class":525},"'link'",[423,1062,1063],{"class":453},"); l.rel ",[423,1065,823],{"class":435},[423,1067,1068],{"class":525}," 'prefetch'",[423,1070,1071],{"class":453},"; l.href ",[423,1073,823],{"class":435},[423,1075,1076],{"class":453}," href; document.head.",[423,1078,1079],{"class":449},"append",[423,1081,1082],{"class":453},"(l);\n",[423,1084,1085],{"class":136,"line":567},[423,1086,1087],{"class":453},"      }\n",[423,1089,1090],{"class":136,"line":597},[423,1091,1092],{"class":453},"    });\n",[423,1094,1095],{"class":136,"line":603},[423,1096,1097],{"class":453},"  }\n",[423,1099,1100,1103,1106],{"class":136,"line":609},[423,1101,1102],{"class":453},"}, { rootMargin: ",[423,1104,1105],{"class":525},"'200px'",[423,1107,875],{"class":453},[423,1109,1110,1112,1115,1117,1120,1123,1126,1128,1130,1132,1134,1137,1140],{"class":136,"line":615},[423,1111,794],{"class":453},[423,1113,1114],{"class":449},"querySelectorAll",[423,1116,469],{"class":453},[423,1118,1119],{"class":525},"'a[data-prefetch]'",[423,1121,1122],{"class":453},").",[423,1124,1125],{"class":449},"forEach",[423,1127,935],{"class":453},[423,1129,19],{"class":472},[423,1131,810],{"class":453},[423,1133,582],{"class":435},[423,1135,1136],{"class":453}," io.",[423,1138,1139],{"class":449},"observe",[423,1141,1142],{"class":453},"(a));\n",[423,1144,1145],{"class":136,"line":878},[423,1146,1147],{"class":428},"\u002F\u002F trade-off: limit viewport prefetching to links you mark explicitly. On a\n",[423,1149,1150],{"class":136,"line":884},[423,1151,1152],{"class":428},"\u002F\u002F listing page with 60 product links, prefetching all of them wastes far more\n",[423,1154,1155],{"class":136,"line":890},[423,1156,1157],{"class":428},"\u002F\u002F than it saves — prefetch the product route chunk once, not per link.\n",[15,1159,1160],{},"Expected outcome: the next likely route's code is in the HTTP cache before the user decides to go there.",[409,1162,1164],{"id":1163},"_4-prefetch-data-alongside-code-for-data-heavy-routes","4. Prefetch data alongside code for data-heavy routes",[15,1166,1167],{},"On hover intent, also warm the route's primary API response (with a short-lived cache in your data layer), so the click finds both code and data ready.",[15,1169,1170],{},[40,1171,47,1174,47,1177,47,1180,47,1182,47,1199,47,1202,47,1204,47,1209,47,1215,47,1219,47,1221,47,1224,47,1228,47,1231,47,1234,47,1237,47,1241,47,1244,47,1247,47,1251,47,1254,47,1257,47,1261,47,1263,47,1266,47,1274,47,1278,47,1282,47],{"viewBox":1172,"width":43,"role":44,"ariaLabel":1173,"style":46},"0 0 760 149","Flow from a user intent signal through guards to prefetching both route code and route data.",[49,1175],{"className":1176,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,1178,1179],{},"Intent-driven prefetch pipeline",[60,1181,1173],{},[1183,1184,1185],"defs",{},[1186,1187,1194],"marker",{"id":1188,"viewBox":1189,"refX":1190,"refY":1191,"markerWidth":1192,"markerHeight":1192,"orient":1193},"fa38d16215","0 0 10 10","9","5","7","auto-start-reverse",[1195,1196],"path",{"d":1197,"fill":69,"style":1198},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[49,1200],{"x":64,"y":64,"width":65,"height":1201,"rx":67,"fill":68,"stroke":69,"style":70},"147",[72,1203,1179],{"x":74,"y":75,"fill":69,"style":76},[49,1205],{"x":74,"y":85,"width":1206,"height":1207,"rx":1208,"fill":103,"stroke":103,"style":104},"108.8","71.0","6",[72,1210,1214],{"x":1211,"y":1212,"fill":69,"style":1213,"textAnchor":97},"82.4","78.0","font-size:13px;font-weight:700","Intent",[72,1216,1218],{"x":1211,"y":1217,"fill":69,"style":330,"textAnchor":97},"96.0","hover \u002F focus \u002F",[72,1220,35],{"x":1211,"y":120,"fill":69,"style":330,"textAnchor":97},[49,1222],{"x":1223,"y":85,"width":1206,"height":1207,"rx":1208,"fill":103,"stroke":103,"style":104},"176.8",[72,1225,1227],{"x":1226,"y":1212,"fill":69,"style":1213,"textAnchor":97},"231.2","Guards",[72,1229,1230],{"x":1226,"y":1217,"fill":69,"style":330,"textAnchor":97},"saveData, 2g,",[72,1232,1233],{"x":1226,"y":120,"fill":69,"style":330,"textAnchor":97},"already done",[49,1235],{"x":1236,"y":85,"width":1206,"height":1207,"rx":1208,"fill":89,"stroke":90,"style":91},"325.6",[72,1238,1240],{"x":1239,"y":1212,"fill":69,"style":1213,"textAnchor":97},"380.0","Route code",[72,1242,1243],{"x":1239,"y":1217,"fill":69,"style":330,"textAnchor":97},"import() + deps",[49,1245],{"x":1246,"y":85,"width":1206,"height":1207,"rx":1208,"fill":89,"stroke":90,"style":91},"474.4",[72,1248,1250],{"x":1249,"y":1212,"fill":69,"style":1213,"textAnchor":97},"528.8","Route data",[72,1252,1253],{"x":1249,"y":1217,"fill":69,"style":330,"textAnchor":97},"warm API cache",[49,1255],{"x":1256,"y":85,"width":1206,"height":1207,"rx":1208,"fill":103,"stroke":103,"style":104},"623.2",[72,1258,1260],{"x":1259,"y":1212,"fill":69,"style":1213,"textAnchor":97},"677.6","Click",[72,1262,117],{"x":1259,"y":1217,"fill":69,"style":330,"textAnchor":97},[72,1264,1265],{"x":1259,"y":120,"fill":69,"style":330,"textAnchor":97},"immediately",[136,1267],{"x1":1268,"y1":1269,"x2":1270,"y2":1269,"stroke":69,"strokeWidth":1271,"style":1272,"markerEnd":1273},"136.8","91.5","174.8","1.5","stroke-opacity:0.6","url(#fa38d16215)",[136,1275],{"x1":1276,"y1":1269,"x2":1277,"y2":1269,"stroke":69,"strokeWidth":1271,"style":1272,"markerEnd":1273},"285.6","323.6",[136,1279],{"x1":1280,"y1":1269,"x2":1281,"y2":1269,"stroke":69,"strokeWidth":1271,"style":1272,"markerEnd":1273},"434.4","472.4",[136,1283],{"x1":1284,"y1":1269,"x2":1285,"y2":1269,"stroke":69,"strokeWidth":1271,"style":1272,"markerEnd":1273},"583.2","621.2",[194,1287,1289],{"id":1288},"verification","Verification",[15,1291,1292,1293,1297],{},"In DevTools, hover a link and watch the Network panel: the route chunk (and its dependencies) should be requested once, then served from cache on click. Measure route transition time with User Timing marks before and after, as in ",[19,1294,1296],{"href":1295},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Ftracking-route-transition-time-with-user-timing\u002F","tracking route transition time with User Timing","; the code phase should drop to near zero for prefetched routes. Check that the current page's LCP did not regress — prefetches should start after it.",[194,1299,1301],{"id":1300},"worked-example-a-documentation-site","Worked Example: A Documentation Site",[15,1303,1304],{},"A docs site split each section into its own chunk. Field data showed first visits to a section taking a median 520ms to render on mobile, of which 340ms was chunk download. The team added hover\u002Ffocus\u002Fpointerdown prefetching for sidebar links and idle-time viewport prefetching for the \"Next page\" link at the bottom of each article. Prefetches were limited to one chunk per section and disabled under data saver. Median transition time on first section visits fell to 210ms, while total bytes per session rose by only 4% — most prefetches were used, because sidebar hovers and \"next page\" visibility are strong predictors of the next click.",[194,1306,1308],{"id":1307},"common-mistakes","Common Mistakes",[199,1310,1311,1320,1330,1336],{},[202,1312,1313,1316,1317,1319],{},[205,1314,1315],{},"Prefetching during initial load."," Wait until after ",[33,1318,473],{}," (or LCP) before any speculative fetch.",[202,1321,1322,1329],{},[205,1323,1324,1325,1328],{},"Using ",[33,1326,1327],{},"rel=\"preload\""," for speculative chunks."," Preload is high priority and for the current page; prefetch is idle priority and for the next one.",[202,1331,1332,1335],{},[205,1333,1334],{},"Prefetching per link instead of per route."," Many links share one route chunk; deduplicate by chunk.",[202,1337,1338,1341],{},[205,1339,1340],{},"Ignoring logged-out vs logged-in differences."," Prefetching authenticated routes for logged-out users wastes requests that will redirect.",[194,1343,1345],{"id":1344},"measuring-prefetch-effectiveness","Measuring Prefetch Effectiveness",[15,1347,1348],{},"Prefetching is a bet, so measure the payoff. Record two numbers per session in RUM: how many route chunks were prefetched, and how many of those were subsequently used by a navigation. A hit rate above roughly 50% means triggers are well chosen; below 20% means you are mostly spending users' bandwidth on guesses, and triggers should be narrowed. Also compare transition time for navigations that hit a prefetched chunk against those that did not — the difference is the value of each successful prefetch, which lets you weigh it against the bytes spent on misses.",[194,1350,1352],{"id":1351},"faq","FAQ",[1354,1355,1358,1362],"details",{"className":1356},[1357],"faq-item",[1359,1360,1361],"summary",{},"How is this different from Speculation Rules?",[15,1363,1364,1365,529],{},"Speculation Rules prefetch or prerender whole documents for multi-page navigations. Route-chunk prefetching warms JavaScript for client-side navigations within an SPA. They solve the same problem at different layers; an SPA benefits from chunk prefetching, a multi-page site from ",[19,1366,1368],{"href":1367},"\u002Fnetwork-protocol-optimization\u002Fspeculative-loading-and-prefetching\u002Fspeculation-rules-for-instant-navigations\u002F","speculation rules for instant navigations",[1354,1370,1372,1375],{"className":1371},[1357],[1359,1373,1374],{},"Does prefetching affect INP?",[15,1376,1377,1378,1380,1381,1384],{},"Downloading in the background does not block the main thread. Evaluating a prefetched module does, so prefer fetching (via ",[33,1379,912],{},") over executing ",[33,1382,1383],{},"import()"," for speculative work, and keep heavy top-level code out of route modules so evaluating them on click stays cheap.",[1354,1386,1388,1391],{"className":1387},[1357],[1359,1389,1390],{},"What about Quicklink-style libraries?",[15,1392,1393,1394,529],{},"Libraries like Quicklink prefetch links in the viewport during idle time with sensible guards, mainly for multi-page sites. They are a good starting point; for SPAs, framework-integrated prefetching that knows your route-to-chunk mapping is more precise. See ",[19,1395,1397],{"href":1396},"\u002Fnetwork-protocol-optimization\u002Fspeculative-loading-and-prefetching\u002Fviewport-prefetching-with-quicklink\u002F","viewport prefetching with Quicklink",[194,1399,1401],{"id":1400},"related","Related",[199,1403,1404,1411,1418],{},[202,1405,1406,1410],{},[19,1407,1409],{"href":1408},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Ffixing-waterfalls-from-nested-dynamic-imports\u002F","Fixing waterfalls from nested dynamic imports"," — the chains prefetching must cover.",[202,1412,1413,1417],{},[19,1414,1416],{"href":1415},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fvite-and-rollup-build-optimization\u002Fcontrolling-vite-modulepreload-for-dynamic-imports\u002F","Controlling Vite modulepreload for dynamic imports"," — how dependency preloads interact with prefetching.",[202,1419,1420,1424],{},[19,1421,1423],{"href":1422},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fcode-splitting-vue-router-routes\u002F","Code-splitting Vue Router routes"," — creating the chunks to prefetch.",[1426,1427,1429],"script",{"type":1428},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Prefetch Route Chunks on Hover and Viewport\",\n  \"description\": \"Strategies for warming code-split route chunks before the click — viewport, hover, pointerdown — with guards for data saver and slow networks.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Build a single prefetch function with network guards\",\n      \"text\": \"Expected outcome: one entry point for all prefetching, with consistent guards.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Prefetch on intent: hover, focus and pointerdown\",\n      \"text\": \"function onIntent(e) { const a = e.target.closest('a[href]'); const load = a && routes[new URL(a.href).pathname]; if (load) prefetchRoute(load); } let hoverTimer; document.addEventListener('mouseover', (e) => { clearTimeout(hoverTimer); hoverTimer = setTimeout(() => onIntent(e), 65); }); document.ad\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Prefetch likely next routes in the viewport at idle priority\",\n      \"text\": \"For primary calls to action visible on the page, use IntersectionObserver and low-priority for the route's files from the build manifest.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Prefetch data alongside code for data-heavy routes\",\n      \"text\": \"On hover intent, also warm the route's primary API response (with a short-lived cache in your data layer), so the click finds both code and data ready.\"\n    }\n  ]\n}\n",[1426,1431,1432],{"type":1428},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Prefetch Route Chunks on Hover and Viewport\",\n  \"description\": \"Strategies for warming code-split route chunks before the click — viewport, hover, pointerdown — with guards for data saver and slow networks.\",\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\u002Fprefetching-route-chunks-on-hover-and-viewport\u002F\"\n  }\n}\n",[1426,1434,1435],{"type":1428},"\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\": \"Prefetching Route Chunks on Hover and Viewport\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport\u002F\"\n    }\n  ]\n}\n",[1437,1438,1439],"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 .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}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 .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":419,"searchDepth":432,"depth":432,"links":1441},[1442,1443,1444,1450,1451,1452,1453,1454,1455],{"id":196,"depth":432,"text":197},{"id":241,"depth":432,"text":242},{"id":406,"depth":432,"text":407,"children":1445},[1446,1447,1448,1449],{"id":411,"depth":457,"text":412},{"id":624,"depth":457,"text":625},{"id":901,"depth":457,"text":902},{"id":1163,"depth":457,"text":1164},{"id":1288,"depth":432,"text":1289},{"id":1300,"depth":432,"text":1301},{"id":1307,"depth":432,"text":1308},{"id":1344,"depth":432,"text":1345},{"id":1351,"depth":432,"text":1352},{"id":1400,"depth":432,"text":1401},"Strategies for warming code-split route chunks before the click — viewport, hover, pointerdown — with guards for data saver and slow networks.","md",{"slug":1459,"type":1460,"breadcrumb":1461,"datePublished":1469,"dateModified":1469},"prefetching-route-chunks-on-hover-and-viewport","article",[1462,1465,1466,1467],{"name":1463,"url":1464},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1468},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport\u002F","2026-10-06","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport",{"title":5,"description":1472},"Prefetch code-split route chunks when links enter the viewport or are hovered, so navigations feel instant without wasting bandwidth on slow connections.","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport\u002Findex","jsPu10bgI2NUEk2FkwHUlHLGl57oAmh8NkEiYLvkedI",[1476,1480],{"title":1477,"path":1478,"stem":1479},"Lazy-Loading React Components with Suspense","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense\u002Findex",{"title":1481,"path":1482,"stem":1483},"Vite vs webpack bundle splitting performance","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fvite-vs-webpack-bundle-splitting-performance","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fvite-vs-webpack-bundle-splitting-performance\u002Findex",1791308076108]