[{"data":1,"prerenderedAt":1267},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense":1258},{"id":4,"title":5,"body":6,"description":1238,"extension":1239,"meta":1240,"navigation":426,"path":1252,"seo":1253,"stem":1256,"__hash__":1257},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense\u002Findex.md","Lazy-Loading React Components with Suspense",{"type":7,"value":8,"toc":1222},"minimark",[9,14,29,40,153,158,187,191,200,206,212,218,333,337,342,670,673,677,689,762,765,769,776,780,786,916,920,923,927,930,934,968,972,979,1113,1117,1141,1155,1164,1180,1184,1207,1212,1215,1218],[10,11,13],"h1",{"id":12},"how-to-lazy-load-react-components-with-reactlazy-and-suspense","How to Lazy-Load React Components with React.lazy and Suspense",[15,16,17,18,23,24,28],"p",{},"This guide 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"," below the route level. Route splitting handles the big boundaries; inside a route, a few components often carry most of the weight — a rich-text editor, a chart, a map, a date-range picker, a modal with a complex form. If they are not visible or interactive at first render, they do not need to be in the route's initial chunk.",[15,30,31,35,36,39],{},[32,33,34],"code",{},"React.lazy"," wraps a dynamic import in a component, and ",[32,37,38],{},"\u003CSuspense>"," provides the fallback shown while it loads. The pattern is simple; the performance pitfalls are in the details. Fallbacks that are smaller than the real component cause layout shift. Lazy components that render immediately on load create a waterfall that delays content. And a lazy component that is first requested on click makes the interaction wait for a download, which INP measures as presentation delay.",[15,41,42],{},[43,44,50,51,50,58,50,62,50,65,50,74,50,80,50,89,50,95,50,100,50,105,50,108,50,111,50,114,50,117,50,120,50,123,50,128,50,132,50,134,50,138,50,140,50,143,50,145,50,148,50,150,50],"svg",{"viewBox":45,"width":46,"role":47,"ariaLabel":48,"style":49},"0 0 760 228","100%","img","Comparison of components that benefit from lazy loading with those that should stay in the initial chunk.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[52,53],"rect",{"className":54,"x":56,"y":56,"width":46,"height":46,"fill":57},[55],"svg-canvas","0","#ffffff",[59,60,61],"title",{},"Good and bad candidates for React.lazy",[63,64,48],"desc",{},[52,66],{"x":67,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[75,76,61],"text",{"x":77,"y":78,"fill":72,"style":79},"28.0","34.0","font-size:16px;font-weight:700",[52,81],{"x":77,"y":82,"width":83,"height":84,"rx":85,"fill":86,"stroke":87,"style":88},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[75,90,94],{"x":91,"y":92,"fill":72,"style":93},"42.0","82.0","font-size:14px;font-weight:700","Lazy-load these",[75,96,99],{"x":91,"y":97,"fill":72,"style":98},"108.0","font-size:12.5px;font-weight:700","•",[75,101,104],{"x":82,"y":97,"fill":72,"style":102,"textAnchor":103},"font-size:12.5px","start","Editors, charts, maps, PDF viewers",[75,106,99],{"x":91,"y":107,"fill":72,"style":98},"132.0",[75,109,110],{"x":82,"y":107,"fill":72,"style":102,"textAnchor":103},"Modals and drawers opened by a click",[75,112,99],{"x":91,"y":113,"fill":72,"style":98},"156.0",[75,115,116],{"x":82,"y":113,"fill":72,"style":102,"textAnchor":103},"Below-the-fold sections with heavy deps",[75,118,99],{"x":91,"y":119,"fill":72,"style":98},"180.0",[75,121,122],{"x":82,"y":119,"fill":72,"style":102,"textAnchor":103},"Admin-only or rarely used panels",[52,124],{"x":125,"y":82,"width":83,"height":84,"rx":85,"fill":126,"stroke":126,"style":127},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[75,129,131],{"x":130,"y":92,"fill":72,"style":93},"406.0","Keep these eager",[75,133,99],{"x":130,"y":97,"fill":72,"style":98},[75,135,137],{"x":136,"y":97,"fill":72,"style":102,"textAnchor":103},"420.0","The LCP element and its parents",[75,139,99],{"x":130,"y":107,"fill":72,"style":98},[75,141,142],{"x":136,"y":107,"fill":72,"style":102,"textAnchor":103},"Small components (under a few KB)",[75,144,99],{"x":130,"y":113,"fill":72,"style":98},[75,146,147],{"x":136,"y":113,"fill":72,"style":102,"textAnchor":103},"Controls users touch immediately",[75,149,99],{"x":130,"y":119,"fill":72,"style":98},[75,151,152],{"x":136,"y":119,"fill":72,"style":102,"textAnchor":103},"Anything rendered on every route",[154,155,157],"h2",{"id":156},"rapid-diagnosis","Rapid Diagnosis",[159,160,161,169,175,181],"ul",{},[162,163,164,168],"li",{},[165,166,167],"strong",{},"Find heavy components in the route chunk."," Use a bundle analyser to find large dependencies and trace them to the components that import them.",[162,170,171,174],{},[165,172,173],{},"Check whether they render initially."," A heavy chart rendered above the fold on load gains little from lazy loading; a chart in a collapsed tab gains a lot.",[162,176,177,180],{},[165,178,179],{},"Look for layout shift when lazy content appears."," If CLS is attributed to a container that holds a lazy component, the fallback is the wrong size.",[162,182,183,186],{},[165,184,185],{},"Check interaction latency on first open."," Clicking \"Edit\" and waiting for an editor chunk shows as a long presentation delay on that interaction.",[154,188,190],{"id":189},"root-cause-analysis","Root Cause Analysis",[15,192,193,196,197,199],{},[165,194,195],{},"1. Lazy components on the critical path."," Wrapping an above-the-fold component in ",[32,198,34],{}," delays its content by a round trip after the route chunk evaluates — a waterfall that harms LCP.",[15,201,202,205],{},[165,203,204],{},"2. Fallbacks with the wrong size."," A spinner fallback 40px tall replaced by a 400px chart pushes everything below it down.",[15,207,208,211],{},[165,209,210],{},"3. Loading on click."," Modals and editors loaded only when opened make the opening interaction wait for download and evaluation.",[15,213,214,217],{},[165,215,216],{},"4. Too many tiny lazy components."," Each lazy boundary is a chunk and a request; splitting small components adds overhead without saving anything meaningful.",[15,219,220],{},[43,221,50,224,50,227,50,230,50,232,50,235,50,237,50,246,50,250,50,257,50,261,50,268,50,272,50,276,50,280,50,283,50,285,50,289,50,294,50,300,50,304,50,307,50,311,50,315,50,319,50,323,50,327,50],{"viewBox":222,"width":46,"role":47,"ariaLabel":223,"style":49},"0 0 760 200","Two timelines of opening a modal with a lazy-loaded editor, comparing loading on click with preloading on hover.",[52,225],{"className":226,"x":56,"y":56,"width":46,"height":46,"fill":57},[55],[59,228,229],{},"Opening a lazy editor: on click vs preloaded on hover",[63,231,223],{},[52,233],{"x":67,"y":67,"width":68,"height":234,"rx":70,"fill":71,"stroke":72,"style":73},"198",[75,236,229],{"x":77,"y":78,"fill":72,"style":79},[238,239],"line",{"x1":240,"y1":241,"x2":240,"y2":113,"stroke":87,"strokeWidth":242,"strokeDashArray":243,"style":245},"295.3","72.0","1.5",[244,244],"4","stroke-opacity:0.9",[75,247,249],{"x":77,"y":248,"fill":72,"style":98},"96.0","On click",[52,251],{"x":252,"y":253,"width":254,"height":255,"rx":256,"fill":126,"stroke":126,"style":127},"123.8","78.0","24.3","26.0","3",[52,258],{"x":259,"y":253,"width":260,"height":255,"rx":256,"fill":86,"stroke":87,"style":88},"149.6","360.3",[75,262,267],{"x":263,"y":264,"fill":72,"style":265,"textAnchor":266},"329.8","95.5","font-size:11.5px","middle","editor chunk",[52,269],{"x":270,"y":253,"width":271,"height":255,"rx":256,"fill":126,"stroke":126,"style":127},"511.4","93.3",[75,273,275],{"x":274,"y":264,"fill":72,"style":265,"textAnchor":266},"558.0","render",[75,277,279],{"x":77,"y":278,"fill":72,"style":98},"134.0","Hover preload",[52,281],{"x":252,"y":282,"width":254,"height":255,"rx":256,"fill":126,"stroke":126,"style":127},"116.0",[52,284],{"x":259,"y":282,"width":271,"height":255,"rx":256,"fill":126,"stroke":126,"style":127},[75,286,275],{"x":287,"y":288,"fill":72,"style":265,"textAnchor":266},"196.2","133.5",[238,290],{"x1":291,"y1":113,"x2":292,"y2":113,"stroke":72,"style":293},"123.0","726.0","stroke-opacity:0.4",[75,295,299],{"x":296,"y":297,"fill":298,"style":265,"textAnchor":266},"133.7","173.0","#51617a","0ms",[75,301,303],{"x":302,"y":297,"fill":298,"style":265,"textAnchor":266},"209.2","100ms",[75,305,306],{"x":240,"y":297,"fill":298,"style":265,"textAnchor":266},"200ms",[75,308,310],{"x":309,"y":297,"fill":298,"style":265,"textAnchor":266},"381.4","300ms",[75,312,314],{"x":313,"y":297,"fill":298,"style":265,"textAnchor":266},"467.6","400ms",[75,316,318],{"x":317,"y":297,"fill":298,"style":265,"textAnchor":266},"553.7","500ms",[75,320,322],{"x":321,"y":297,"fill":298,"style":265,"textAnchor":266},"639.9","600ms",[75,324,326],{"x":325,"y":297,"fill":298,"style":265,"textAnchor":266},"715.3","700ms",[75,328,332],{"x":240,"y":329,"fill":330,"style":331,"textAnchor":266},"68.0","#8a6608","font-size:12px;font-weight:700","INP 200ms",[154,334,336],{"id":335},"step-by-step-resolution","Step-by-Step Resolution",[338,339,341],"h3",{"id":340},"_1-lazy-load-heavy-not-immediately-visible-components","1. Lazy-load heavy, not-immediately-visible components",[343,344,349],"pre",{"className":345,"code":346,"language":347,"meta":348,"style":348},"language-jsx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","import { lazy, Suspense, useState } from 'react';\nconst loadEditor = () => import('.\u002FRichTextEditor');\nconst RichTextEditor = lazy(loadEditor);\n\nexport function CommentBox() {\n  const [editing, setEditing] = useState(false);\n  return editing ? (\n    \u003CSuspense fallback={\u003Cdiv className=\"editor-shell\" style={{ minHeight: 240 }} aria-busy=\"true\" \u002F>}>\n      \u003CRichTextEditor \u002F>\n    \u003C\u002FSuspense>\n  ) : (\n    \u003Cbutton onMouseEnter={loadEditor} onFocus={loadEditor} onClick={() => setEditing(true)}>Write a comment\u003C\u002Fbutton>\n  );\n}\n\u002F\u002F trade-off: the fallback reserves 240px. If the editor's height varies by\n\u002F\u002F toolbar configuration, measure it and match; a mismatched shell is a CLS bug.\n","jsx","",[32,350,351,373,404,421,428,443,477,492,555,567,577,588,645,651,657,664],{"__ignoreMap":348},[352,353,355,359,363,366,370],"span",{"class":238,"line":354},1,[352,356,358],{"class":357},"sPARh","import",[352,360,362],{"class":361},"saISM"," { lazy, Suspense, useState } ",[352,364,365],{"class":357},"from",[352,367,369],{"class":368},"sZ8jY"," 'react'",[352,371,372],{"class":361},";\n",[352,374,376,379,383,386,389,392,395,398,401],{"class":238,"line":375},2,[352,377,378],{"class":357},"const",[352,380,382],{"class":381},"smZ65"," loadEditor",[352,384,385],{"class":357}," =",[352,387,388],{"class":361}," () ",[352,390,391],{"class":357},"=>",[352,393,394],{"class":357}," import",[352,396,397],{"class":361},"(",[352,399,400],{"class":368},"'.\u002FRichTextEditor'",[352,402,403],{"class":361},");\n",[352,405,407,409,413,415,418],{"class":238,"line":406},3,[352,408,378],{"class":357},[352,410,412],{"class":411},"sPXB4"," RichTextEditor",[352,414,385],{"class":357},[352,416,417],{"class":381}," lazy",[352,419,420],{"class":361},"(loadEditor);\n",[352,422,424],{"class":238,"line":423},4,[352,425,427],{"emptyLinePlaceholder":426},true,"\n",[352,429,431,434,437,440],{"class":238,"line":430},5,[352,432,433],{"class":357},"export",[352,435,436],{"class":357}," function",[352,438,439],{"class":381}," CommentBox",[352,441,442],{"class":361},"() {\n",[352,444,446,449,452,455,458,461,464,467,470,472,475],{"class":238,"line":445},6,[352,447,448],{"class":357},"  const",[352,450,451],{"class":361}," [",[352,453,454],{"class":411},"editing",[352,456,457],{"class":361},", ",[352,459,460],{"class":411},"setEditing",[352,462,463],{"class":361},"] ",[352,465,466],{"class":357},"=",[352,468,469],{"class":381}," useState",[352,471,397],{"class":361},[352,473,474],{"class":411},"false",[352,476,403],{"class":361},[352,478,480,483,486,489],{"class":238,"line":479},7,[352,481,482],{"class":357},"  return",[352,484,485],{"class":361}," editing ",[352,487,488],{"class":357},"?",[352,490,491],{"class":361}," (\n",[352,493,495,498,502,505,508,511,514,517,519,522,525,527,530,533,536,539,542,544,547,550,552],{"class":238,"line":494},8,[352,496,497],{"class":361},"    \u003C",[352,499,501],{"class":500},"sZBmE","Suspense",[352,503,504],{"class":411}," fallback",[352,506,507],{"class":357},"={",[352,509,510],{"class":361},"\u003C",[352,512,513],{"class":500},"div",[352,515,516],{"class":411}," className",[352,518,466],{"class":357},[352,520,521],{"class":368},"\"editor-shell\"",[352,523,524],{"class":411}," style",[352,526,507],{"class":357},[352,528,529],{"class":361},"{ minHeight: ",[352,531,532],{"class":411},"240",[352,534,535],{"class":361}," }",[352,537,538],{"class":357},"}",[352,540,541],{"class":411}," aria-busy",[352,543,466],{"class":357},[352,545,546],{"class":368},"\"true\"",[352,548,549],{"class":361}," \u002F>",[352,551,538],{"class":357},[352,553,554],{"class":361},">\n",[352,556,558,561,564],{"class":238,"line":557},9,[352,559,560],{"class":361},"      \u003C",[352,562,563],{"class":500},"RichTextEditor",[352,565,566],{"class":361}," \u002F>\n",[352,568,570,573,575],{"class":238,"line":569},10,[352,571,572],{"class":361},"    \u003C\u002F",[352,574,501],{"class":500},[352,576,554],{"class":361},[352,578,580,583,586],{"class":238,"line":579},11,[352,581,582],{"class":361},"  ) ",[352,584,585],{"class":357},":",[352,587,491],{"class":361},[352,589,591,593,596,599,601,604,606,609,611,613,615,618,620,623,625,628,630,633,636,638,641,643],{"class":238,"line":590},12,[352,592,497],{"class":361},[352,594,595],{"class":500},"button",[352,597,598],{"class":411}," onMouseEnter",[352,600,507],{"class":357},[352,602,603],{"class":361},"loadEditor",[352,605,538],{"class":357},[352,607,608],{"class":411}," onFocus",[352,610,507],{"class":357},[352,612,603],{"class":361},[352,614,538],{"class":357},[352,616,617],{"class":411}," onClick",[352,619,507],{"class":357},[352,621,622],{"class":361},"() ",[352,624,391],{"class":357},[352,626,627],{"class":381}," setEditing",[352,629,397],{"class":361},[352,631,632],{"class":411},"true",[352,634,635],{"class":361},")",[352,637,538],{"class":357},[352,639,640],{"class":361},">Write a comment\u003C\u002F",[352,642,595],{"class":500},[352,644,554],{"class":361},[352,646,648],{"class":238,"line":647},13,[352,649,650],{"class":361},"  );\n",[352,652,654],{"class":238,"line":653},14,[352,655,656],{"class":361},"}\n",[352,658,660],{"class":238,"line":659},15,[352,661,663],{"class":662},"sjfSM","\u002F\u002F trade-off: the fallback reserves 240px. If the editor's height varies by\n",[352,665,667],{"class":238,"line":666},16,[352,668,669],{"class":662},"\u002F\u002F toolbar configuration, measure it and match; a mismatched shell is a CLS bug.\n",[15,671,672],{},"Expected outcome: the editor's dependencies (often 100KB+) leave the route chunk; the hover preload hides most of the download.",[338,674,676],{"id":675},"_2-size-fallbacks-to-the-final-component","2. Size fallbacks to the final component",[15,678,679,680,683,684,688],{},"Fallbacks should occupy the same space as the loaded component: fixed heights for editors, ",[32,681,682],{},"aspect-ratio"," for charts and maps. See ",[19,685,687],{"href":686},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002F","fixing CLS from skeleton screens that resize",".",[343,690,694],{"className":691,"code":692,"language":693,"meta":348,"style":348},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast",".chart-shell { aspect-ratio: 16 \u002F 9; background: var(--surface-2); border-radius: 8px; }\n\u002F* trade-off: a fixed aspect ratio assumes the chart fills its container\n   width. Charts with legends that wrap at small widths need a min-height too. *\u002F\n","css",[32,695,696,752,757],{"__ignoreMap":348},[352,697,698,701,704,706,709,712,715,718,721,724,726,729,731,735,738,741,743,746,749],{"class":238,"line":354},[352,699,700],{"class":411},".chart-shell",[352,702,703],{"class":361}," { ",[352,705,682],{"class":411},[352,707,708],{"class":361},": ",[352,710,711],{"class":411},"16",[352,713,714],{"class":361}," \u002F ",[352,716,717],{"class":411},"9",[352,719,720],{"class":361},"; ",[352,722,723],{"class":411},"background",[352,725,708],{"class":361},[352,727,728],{"class":411},"var",[352,730,397],{"class":361},[352,732,734],{"class":733},"sQw3B","--surface-2",[352,736,737],{"class":361},"); ",[352,739,740],{"class":411},"border-radius",[352,742,708],{"class":361},[352,744,745],{"class":411},"8",[352,747,748],{"class":357},"px",[352,750,751],{"class":361},"; }\n",[352,753,754],{"class":238,"line":375},[352,755,756],{"class":662},"\u002F* trade-off: a fixed aspect ratio assumes the chart fills its container\n",[352,758,759],{"class":238,"line":406},[352,760,761],{"class":662},"   width. Charts with legends that wrap at small widths need a min-height too. *\u002F\n",[15,763,764],{},"Expected outcome: no layout shift when lazy content resolves.",[338,766,768],{"id":767},"_3-preload-on-intent-not-on-render","3. Preload on intent, not on render",[15,770,771,772,775],{},"Call the import function on hover, focus or when the trigger scrolls into view, so the chunk is ready when the component renders. Because ",[32,773,774],{},"import()"," is cached, calling it multiple times is harmless.",[338,777,779],{"id":778},"_4-group-boundaries-sensibly","4. Group boundaries sensibly",[15,781,782,783,785],{},"Use one ",[32,784,501],{}," boundary per independently loading region, not one per lazy component. Multiple lazy components in one region should resolve together under a single fallback, so the region does not pop in piece by piece.",[15,787,788],{},[43,789,50,792,50,795,50,798,50,800,50,816,50,819,50,821,50,826,50,831,50,834,50,838,50,845,50,851,50,855,50,860,50,863,50,867,50,869,50,872,50,875,50,878,50,881,50,884,50,887,50,891,50,893,50,896,50,899,50,902,50,906,50,909,50,912,50],{"viewBox":790,"width":46,"role":47,"ariaLabel":791,"style":49},"0 0 760 325","Decision sequence for whether a React component should be lazy-loaded.",[52,793],{"className":794,"x":56,"y":56,"width":46,"height":46,"fill":57},[55],[59,796,797],{},"Should this component be lazy?",[63,799,791],{},[801,802,803],"defs",{},[804,805,811],"marker",{"id":806,"viewBox":807,"refX":717,"refY":808,"markerWidth":809,"markerHeight":809,"orient":810},"fa758a1664","0 0 10 10","5","7","auto-start-reverse",[812,813],"path",{"d":814,"fill":72,"style":815},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[52,817],{"x":67,"y":67,"width":68,"height":818,"rx":70,"fill":71,"stroke":72,"style":73},"323",[75,820,797],{"x":77,"y":78,"fill":72,"style":79},[52,822],{"x":77,"y":82,"width":823,"height":824,"rx":85,"fill":72,"stroke":72,"style":825},"320.0","35.0","fill-opacity:0.06;stroke-opacity:0.4",[75,827,830],{"x":828,"y":253,"fill":72,"style":829,"textAnchor":266},"188.0","font-size:13px;font-weight:700","Rendered above the fold on first load?",[52,832],{"x":833,"y":82,"width":823,"height":824,"rx":85,"fill":86,"stroke":87,"style":88},"412.0",[75,835,837],{"x":836,"y":253,"fill":72,"style":102,"textAnchor":266},"572.0","Keep it eager — lazy adds a waterfall",[238,839],{"x1":840,"y1":841,"x2":842,"y2":841,"stroke":72,"strokeWidth":242,"style":843,"markerEnd":844},"348.0","73.5","410.0","stroke-opacity:0.6","url(#fa758a1664)",[75,846,850],{"x":847,"y":848,"fill":126,"style":849,"textAnchor":266},"380.0","66.5","font-size:11.5px;font-weight:700","yes",[238,852],{"x1":828,"y1":853,"x2":828,"y2":854,"stroke":72,"strokeWidth":242,"style":843,"markerEnd":844},"91.0","125.0",[75,856,859],{"x":857,"y":858,"fill":298,"style":849},"196.0","113.0","no",[52,861],{"x":77,"y":862,"width":823,"height":824,"rx":85,"fill":72,"stroke":72,"style":825},"127.0",[75,864,866],{"x":828,"y":865,"fill":72,"style":829,"textAnchor":266},"149.0","Adds under ~10KB to the chunk?",[52,868],{"x":833,"y":862,"width":823,"height":824,"rx":85,"fill":86,"stroke":87,"style":88},[75,870,871],{"x":836,"y":865,"fill":72,"style":102,"textAnchor":266},"Keep it eager — not worth a request",[238,873],{"x1":840,"y1":874,"x2":842,"y2":874,"stroke":72,"strokeWidth":242,"style":843,"markerEnd":844},"144.5",[75,876,850],{"x":847,"y":877,"fill":126,"style":849,"textAnchor":266},"137.5",[238,879],{"x1":828,"y1":880,"x2":828,"y2":857,"stroke":72,"strokeWidth":242,"style":843,"markerEnd":844},"162.0",[75,882,859],{"x":857,"y":883,"fill":298,"style":849},"184.0",[52,885],{"x":77,"y":886,"width":823,"height":824,"rx":85,"fill":72,"stroke":72,"style":825},"198.0",[75,888,890],{"x":828,"y":889,"fill":72,"style":829,"textAnchor":266},"220.0","Opened by a user action?",[52,892],{"x":833,"y":886,"width":823,"height":824,"rx":85,"fill":126,"stroke":126,"style":127},[75,894,895],{"x":836,"y":889,"fill":72,"style":102,"textAnchor":266},"Lazy + preload on hover\u002Ffocus",[238,897],{"x1":840,"y1":898,"x2":842,"y2":898,"stroke":72,"strokeWidth":242,"style":843,"markerEnd":844},"215.5",[75,900,850],{"x":847,"y":901,"fill":126,"style":849,"textAnchor":266},"208.5",[238,903],{"x1":828,"y1":904,"x2":828,"y2":905,"stroke":72,"strokeWidth":242,"style":843,"markerEnd":844},"233.0","267.0",[75,907,859],{"x":857,"y":908,"fill":298,"style":849},"255.0",[52,910],{"x":77,"y":911,"width":823,"height":78,"rx":85,"fill":86,"stroke":87,"style":88},"269.0",[75,913,915],{"x":828,"y":914,"fill":72,"style":102,"textAnchor":266},"290.5","Lazy + preload when near the viewport",[154,917,919],{"id":918},"verification","Verification",[15,921,922],{},"Compare the route chunk size before and after in the bundle analyser. In a throttled trace, opening the lazy component after hovering should show no network request at click time. Lighthouse and RUM should show no new CLS from lazy containers. For interactions that open lazy components, INP should not regress — if it does, the preload is not firing early enough.",[154,924,926],{"id":925},"worked-example-a-dashboard-route","Worked Example: A Dashboard Route",[15,928,929],{},"A dashboard route chunk weighed 410KB uncompressed, of which a charting library and a date-range picker accounted for 230KB. Charts appeared in tabs, with only the first tab visible initially; the date picker opened in a popover. The team lazy-loaded the charts for non-default tabs (preloading on tab hover) and the date picker (preloading on focus of the date field), keeping the first tab's chart eager because it was above the fold. The route chunk fell to 260KB, lab TBT on the dashboard dropped by 70ms, and opening another tab after hovering showed no measurable delay.",[154,931,933],{"id":932},"common-mistakes","Common Mistakes",[159,935,936,942,948,958],{},[162,937,938,941],{},[165,939,940],{},"Lazy-loading the LCP element."," It moves the most important content behind an extra request.",[162,943,944,947],{},[165,945,946],{},"Spinners as fallbacks for large content."," A small spinner replaced by a large block is a layout shift.",[162,949,950,953,954,957],{},[165,951,952],{},"Creating lazy components inside other components."," Calling ",[32,955,956],{},"lazy()"," inside a render function creates a new component type each render, remounting it and refetching state.",[162,959,960,963,964,688],{},[165,961,962],{},"Forgetting error boundaries."," A chunk that fails to load throws; without an error boundary, the whole tree unmounts. See ",[19,965,967],{"href":966},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fhandling-chunk-load-errors-after-a-deploy\u002F","handling chunk load errors after a deploy",[154,969,971],{"id":970},"suspense-boundaries-and-transitions","Suspense Boundaries and Transitions",[15,973,974,975,978],{},"When state changes cause an already-visible boundary to suspend again — switching tabs that each lazy-load content, for instance — React replaces the visible content with the fallback, which looks like a flash and can shift layout. Wrapping the state update in ",[32,976,977],{},"startTransition"," tells React to keep showing the old content until the new content is ready, avoiding the flash. Combine it with intent-based preloading so the wait is short: preload on tab hover, switch in a transition on click, and the user sees the new tab without ever seeing a fallback.",[343,980,982],{"className":345,"code":981,"language":347,"meta":348,"style":348},"const [tab, setTab] = useState('overview');\nconst [isPending, startTransition] = useTransition();\n\u003CTabButton onMouseEnter={() => import('.\u002FAnalyticsTab')}\n           onClick={() => startTransition(() => setTab('analytics'))} pending={isPending} \u002F>\n\u002F\u002F trade-off: transitions keep stale content on screen while loading; show a\n\u002F\u002F subtle pending indicator so users know their click registered.\n",[32,983,984,1011,1034,1060,1103,1108],{"__ignoreMap":348},[352,985,986,988,990,993,995,998,1000,1002,1004,1006,1009],{"class":238,"line":354},[352,987,378],{"class":357},[352,989,451],{"class":361},[352,991,992],{"class":411},"tab",[352,994,457],{"class":361},[352,996,997],{"class":411},"setTab",[352,999,463],{"class":361},[352,1001,466],{"class":357},[352,1003,469],{"class":381},[352,1005,397],{"class":361},[352,1007,1008],{"class":368},"'overview'",[352,1010,403],{"class":361},[352,1012,1013,1015,1017,1020,1022,1024,1026,1028,1031],{"class":238,"line":375},[352,1014,378],{"class":357},[352,1016,451],{"class":361},[352,1018,1019],{"class":411},"isPending",[352,1021,457],{"class":361},[352,1023,977],{"class":411},[352,1025,463],{"class":361},[352,1027,466],{"class":357},[352,1029,1030],{"class":381}," useTransition",[352,1032,1033],{"class":361},"();\n",[352,1035,1036,1038,1041,1043,1045,1047,1049,1051,1053,1056,1058],{"class":238,"line":406},[352,1037,510],{"class":361},[352,1039,1040],{"class":500},"TabButton",[352,1042,598],{"class":411},[352,1044,507],{"class":357},[352,1046,622],{"class":361},[352,1048,391],{"class":357},[352,1050,394],{"class":357},[352,1052,397],{"class":361},[352,1054,1055],{"class":368},"'.\u002FAnalyticsTab'",[352,1057,635],{"class":361},[352,1059,656],{"class":357},[352,1061,1062,1065,1067,1069,1071,1074,1077,1079,1082,1084,1087,1090,1092,1095,1097,1099,1101],{"class":238,"line":423},[352,1063,1064],{"class":411},"           onClick",[352,1066,507],{"class":357},[352,1068,622],{"class":361},[352,1070,391],{"class":357},[352,1072,1073],{"class":381}," startTransition",[352,1075,1076],{"class":361},"(() ",[352,1078,391],{"class":357},[352,1080,1081],{"class":381}," setTab",[352,1083,397],{"class":361},[352,1085,1086],{"class":368},"'analytics'",[352,1088,1089],{"class":361},"))",[352,1091,538],{"class":357},[352,1093,1094],{"class":411}," pending",[352,1096,507],{"class":357},[352,1098,1019],{"class":361},[352,1100,538],{"class":357},[352,1102,566],{"class":361},[352,1104,1105],{"class":238,"line":430},[352,1106,1107],{"class":662},"\u002F\u002F trade-off: transitions keep stale content on screen while loading; show a\n",[352,1109,1110],{"class":238,"line":445},[352,1111,1112],{"class":662},"\u002F\u002F subtle pending indicator so users know their click registered.\n",[154,1114,1116],{"id":1115},"faq","FAQ",[1118,1119,1122,1126],"details",{"className":1120},[1121],"faq-item",[1123,1124,1125],"summary",{},"Does React.lazy work with server rendering?",[15,1127,1128,1129,1132,1133,1136,1137,1140],{},"In React 18+, ",[32,1130,1131],{},"lazy"," works with streaming SSR: the server renders the fallback or, if the component resolves during rendering, its content, and the client hydrates it when its chunk arrives. Frameworks like Next.js provide ",[32,1134,1135],{},"next\u002Fdynamic"," with options such as ",[32,1138,1139],{},"ssr: false"," for client-only components.",[1118,1142,1144,1147],{"className":1143},[1121],[1123,1145,1146],{},"Is a named export usable with React.lazy?",[15,1148,1149,1151,1152,688],{},[32,1150,34],{}," expects a module with a default export. For named exports, map the import: ",[32,1153,1154],{},"lazy(() => import('.\u002FChart').then((m) => ({ default: m.Chart })))",[1118,1156,1158,1161],{"className":1157},[1121],[1123,1159,1160],{},"How small is too small to lazy-load?",[15,1162,1163],{},"Below roughly 10KB uncompressed, the request overhead and the loading state usually cost more than the bytes saved. Lazy-load components that pull in substantial dependencies, not components that are merely large in source lines.",[1118,1165,1167,1170],{"className":1166},[1121],[1123,1168,1169],{},"Can I preload a lazy component without rendering it?",[15,1171,1172,1173,1175,1176,1179],{},"Yes — keep the import function separate from the ",[32,1174,956],{}," call, as in the examples above, and call it whenever you want the chunk fetched. The module promise is cached, so the later render resolves immediately. Some codebases wrap this in a helper that returns both the lazy component and a ",[32,1177,1178],{},"preload"," function, which keeps the two in sync.",[154,1181,1183],{"id":1182},"related","Related",[159,1185,1186,1193,1200],{},[162,1187,1188,1192],{},[19,1189,1191],{"href":1190},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport\u002F","Prefetching route chunks on hover and viewport"," — the same intent signals at route level.",[162,1194,1195,1199],{},[19,1196,1198],{"href":1197},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fselective-hydration-with-react-suspense\u002F","Selective hydration with React Suspense"," — Suspense boundaries during hydration.",[162,1201,1202,1206],{},[19,1203,1205],{"href":1204},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fwebpack-bundle-analysis-techniques\u002Freducing-vendor-chunk-size-in-a-react-app\u002F","Reducing vendor chunk size in a React app"," — shrinking what remains eager.",[1208,1209,1211],"script",{"type":1210},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Lazy-Load React Components with React.lazy and Suspense\",\n  \"description\": \"When and how to lazy-load React components below the route level, with fallbacks that do not shift layout and preloading that hides latency.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Lazy-load heavy, not-immediately-visible components\",\n      \"text\": \"export function CommentBox() { const [editing, setEditing] = useState(false); return editing ?\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Size fallbacks to the final component\",\n      \"text\": \"Fallbacks should occupy the same space as the loaded component: fixed heights for editors, aspect-ratio for charts and maps.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Preload on intent, not on render\",\n      \"text\": \"Call the import function on hover, focus or when the trigger scrolls into view, so the chunk is ready when the component renders.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Group boundaries sensibly\",\n      \"text\": \"Use one Suspense boundary per independently loading region, not one per lazy component.\"\n    }\n  ]\n}\n",[1208,1213,1214],{"type":1210},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Lazy-Load React Components with React.lazy and Suspense\",\n  \"description\": \"When and how to lazy-load React components below the route level, with fallbacks that do not shift layout and preloading that hides latency.\",\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\u002Flazy-loading-react-components-with-suspense\u002F\"\n  }\n}\n",[1208,1216,1217],{"type":1210},"\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\": \"Lazy-Loading React Components with Suspense\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense\u002F\"\n    }\n  ]\n}\n",[1219,1220,1221],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}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 .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}",{"title":348,"searchDepth":375,"depth":375,"links":1223},[1224,1225,1226,1232,1233,1234,1235,1236,1237],{"id":156,"depth":375,"text":157},{"id":189,"depth":375,"text":190},{"id":335,"depth":375,"text":336,"children":1227},[1228,1229,1230,1231],{"id":340,"depth":406,"text":341},{"id":675,"depth":406,"text":676},{"id":767,"depth":406,"text":768},{"id":778,"depth":406,"text":779},{"id":918,"depth":375,"text":919},{"id":925,"depth":375,"text":926},{"id":932,"depth":375,"text":933},{"id":970,"depth":375,"text":971},{"id":1115,"depth":375,"text":1116},{"id":1182,"depth":375,"text":1183},"When and how to lazy-load React components below the route level, with fallbacks that do not shift layout and preloading that hides latency.","md",{"slug":1241,"type":1242,"breadcrumb":1243,"datePublished":1251,"dateModified":1251},"lazy-loading-react-components-with-suspense","article",[1244,1247,1248,1249],{"name":1245,"url":1246},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1250},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense\u002F","2026-10-06","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense",{"title":1254,"description":1255},"Lazy-Loading React Components with React.lazy and Suspense","Split heavy React components with React.lazy and Suspense, size fallbacks to avoid CLS, preload on intent, and avoid waterfalls and spinners that hurt UX.","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Flazy-loading-react-components-with-suspense\u002Findex","VO0obxCyHBwWCrulmPZvW2pdEfhlMWDokj8hna1MT6A",[1259,1263],{"title":1260,"path":1261,"stem":1262},"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",{"title":1264,"path":1265,"stem":1266},"Prefetching Route Chunks on Hover and Viewport","\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport","javascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fprefetching-route-chunks-on-hover-and-viewport\u002Findex",1791308076276]