[{"data":1,"prerenderedAt":1326},["ShallowReactive",2],{"content:\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react":3,"surroundings:\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react":1317},{"id":4,"title":5,"body":6,"description":1296,"extension":1297,"meta":1298,"navigation":1310,"path":1311,"seo":1312,"stem":1315,"__hash__":1316},"content\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react\u002Findex.md","Virtualizing Long Lists in React",{"type":7,"value":8,"toc":1279},"minimark",[9,14,29,32,137,142,171,175,181,187,193,199,203,208,683,687,785,789,796,800,807,885,889,892,896,899,1029,1033,1075,1079,1105,1109,1115,1121,1127,1133,1137,1149,1158,1167,1176,1185,1198,1210,1219,1228,1237,1241,1264,1269,1272,1275],[10,11,13],"h1",{"id":12},"how-to-virtualize-long-lists-in-react","How to Virtualize Long Lists in React",[15,16,17,18,23,24,28],"p",{},"This guide is part of ",[19,20,22],"a",{"href":21},"\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002F","DOM Size & List Virtualization",", within ",[19,25,27],{"href":26},"\u002Frendering-css-performance\u002F","Rendering & CSS Performance",". A React component that maps over 5,000 orders, log lines or messages renders 5,000 rows, each with several elements. The initial render and commit take hundreds of milliseconds, the DOM holds tens of thousands of nodes, every state change that touches the list makes React reconcile thousands of components, and the browser recalculates style and layout across all of them. Scrolling may be smooth, but filtering, sorting and selecting rows are slow, and INP suffers.",[15,30,31],{},"Virtualization (or \"windowing\") renders only the rows that are visible, plus a small buffer, inside a container sized to the full list height. As the user scrolls, rows outside the window are removed and new ones rendered. DOM size stays constant regardless of list length. Libraries such as TanStack Virtual, react-window and react-virtuoso implement it; the main decisions are fixed vs dynamic row heights, overscan, and how to preserve accessibility and browser features.",[15,33,34],{},[35,36,42,43,42,50,42,54,42,57,42,66,42,72,42,79,42,87,42,92,42,96,42,100,42,104,42,108,42,115,42,119,42,123,42,127,42,131,42],"svg",{"viewBox":37,"width":38,"role":39,"ariaLabel":40,"style":41},"0 0 760 220","100%","img","Bar chart comparing initial render time and filter interaction time for a full list and a virtualized list.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[44,45],"rect",{"className":46,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],"svg-canvas","0","#ffffff",[51,52,53],"title",{},"Rendering 5,000 order rows in React",[55,56,40],"desc",{},[44,58],{"x":59,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64,"style":65},"1","758","218","10","none","currentColor","stroke-opacity:0.18",[67,68,53],"text",{"x":69,"y":70,"fill":64,"style":71},"28.0","34.0","font-size:16px;font-weight:700",[67,73,78],{"x":74,"y":75,"fill":64,"style":76,"textAnchor":77},"173.4","70.0","font-size:13px","end","Full list - initial render",[44,80],{"x":81,"y":82,"width":83,"height":84,"rx":85,"fill":63,"stroke":64,"style":86},"185.4","56.0","482.6","19","3","fill-opacity:0.7;stroke-opacity:0.35",[67,88,91],{"x":89,"y":75,"fill":64,"style":90},"674.0","font-size:12px;font-weight:600","1240ms",[67,93,95],{"x":74,"y":94,"fill":64,"style":76,"textAnchor":77},"101.0","Full list - filter change",[44,97],{"x":81,"y":98,"width":99,"height":84,"rx":85,"fill":63,"stroke":64,"style":86},"87.0","237.4",[67,101,103],{"x":102,"y":94,"fill":64,"style":90},"428.8","610ms",[67,105,107],{"x":74,"y":106,"fill":64,"style":76,"textAnchor":77},"132.0","Virtualized - initial render",[44,109],{"x":81,"y":110,"width":111,"height":84,"rx":85,"fill":112,"stroke":113,"style":114},"118.0","14.8","#ffc300","#b8860b","fill-opacity:0.7;stroke-opacity:0.9",[67,116,118],{"x":117,"y":106,"fill":64,"style":90},"206.2","38ms",[67,120,122],{"x":74,"y":121,"fill":64,"style":76,"textAnchor":77},"163.0","Virtualized - filter change",[44,124],{"x":81,"y":125,"width":126,"height":84,"rx":85,"fill":112,"stroke":113,"style":114},"149.0","9.3",[67,128,130],{"x":129,"y":121,"fill":64,"style":90},"200.8","24ms",[67,132,136],{"x":69,"y":133,"fill":134,"style":135},"198.0","#51617a","font-size:12.5px","Mid-range laptop profile with 4x CPU throttling.",[138,139,141],"h2",{"id":140},"rapid-diagnosis","Rapid Diagnosis",[143,144,145,153,159,165],"ul",{},[146,147,148,152],"li",{},[149,150,151],"strong",{},"Count rendered rows"," in React DevTools or the DOM; hundreds or thousands of rows indicate a candidate.",[146,154,155,158],{},[149,156,157],{},"Profile initial render and interactions"," with the React Profiler and the Performance panel.",[146,160,161,164],{},[149,162,163],{},"Check DOM size"," in Lighthouse; long lists usually dominate.",[146,166,167,170],{},[149,168,169],{},"Check whether users need the full list at once","; pagination may be simpler.",[138,172,174],{"id":173},"root-cause-analysis","Root Cause Analysis",[15,176,177,180],{},[149,178,179],{},"1. Rendering every row."," Cost proportional to list length.",[15,182,183,186],{},[149,184,185],{},"2. Reconciliation on every change."," Selecting or filtering re-renders many row components.",[15,188,189,192],{},[149,190,191],{},"3. Style and layout over many nodes."," Any class change near the list triggers large recalculations.",[15,194,195,198],{},[149,196,197],{},"4. Memory."," Thousands of components and DOM nodes increase GC pressure.",[138,200,202],{"id":201},"step-by-step-resolution","Step-by-Step Resolution",[204,205,207],"h3",{"id":206},"_1-virtualize-with-fixed-row-heights","1. Virtualize with fixed row heights",[209,210,215],"pre",{"className":211,"code":212,"language":213,"meta":214,"style":214},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","import { useVirtualizer } from '@tanstack\u002Freact-virtual';\nfunction Orders({ rows }) {\n  const parentRef = React.useRef(null);\n  const virtualizer = useVirtualizer({\n    count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 48, overscan: 10,\n  });\n  return (\n    \u003Cdiv ref={parentRef} style={{ height: '70vh', overflow: 'auto' }} role=\"list\" aria-label=\"Orders\">\n      \u003Cdiv style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>\n        {virtualizer.getVirtualItems().map((v) => (\n          \u003Cdiv key={v.key} role=\"listitem\" aria-setsize={rows.length} aria-posinset={v.index + 1}\n               style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 48, transform: `translateY(${v.start}px)` }}>\n            \u003COrderRow order={rows[v.index]} \u002F>\n          \u003C\u002Fdiv>\n        ))}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  );\n}\n\u002F\u002F trade-off: overscan of 10 rows hides blank areas on fast scrolls at the cost of\n\u002F\u002F rendering ~20 extra rows; lower it on slow devices if rendering is expensive.\n","javascript","",[216,217,218,241,261,289,305,347,353,362,426,456,487,541,599,621,631,639,649,659,665,670,677],"code",{"__ignoreMap":214},[219,220,223,227,231,234,238],"span",{"class":221,"line":222},"line",1,[219,224,226],{"class":225},"sPARh","import",[219,228,230],{"class":229},"saISM"," { useVirtualizer } ",[219,232,233],{"class":225},"from",[219,235,237],{"class":236},"sZ8jY"," '@tanstack\u002Freact-virtual'",[219,239,240],{"class":229},";\n",[219,242,244,247,251,254,258],{"class":221,"line":243},2,[219,245,246],{"class":225},"function",[219,248,250],{"class":249},"smZ65"," Orders",[219,252,253],{"class":229},"({ ",[219,255,257],{"class":256},"sQw3B","rows",[219,259,260],{"class":229}," }) {\n",[219,262,264,267,271,274,277,280,283,286],{"class":221,"line":263},3,[219,265,266],{"class":225},"  const",[219,268,270],{"class":269},"sPXB4"," parentRef",[219,272,273],{"class":225}," =",[219,275,276],{"class":229}," React.",[219,278,279],{"class":249},"useRef",[219,281,282],{"class":229},"(",[219,284,285],{"class":269},"null",[219,287,288],{"class":229},");\n",[219,290,292,294,297,299,302],{"class":221,"line":291},4,[219,293,266],{"class":225},[219,295,296],{"class":269}," virtualizer",[219,298,273],{"class":225},[219,300,301],{"class":249}," useVirtualizer",[219,303,304],{"class":229},"({\n",[219,306,308,311,314,317,320,323,326,329,332,334,336,339,342,344],{"class":221,"line":307},5,[219,309,310],{"class":229},"    count: rows.",[219,312,313],{"class":269},"length",[219,315,316],{"class":229},", ",[219,318,319],{"class":249},"getScrollElement",[219,321,322],{"class":229},": () ",[219,324,325],{"class":225},"=>",[219,327,328],{"class":229}," parentRef.current, ",[219,330,331],{"class":249},"estimateSize",[219,333,322],{"class":229},[219,335,325],{"class":225},[219,337,338],{"class":269}," 48",[219,340,341],{"class":229},", overscan: ",[219,343,62],{"class":269},[219,345,346],{"class":229},",\n",[219,348,350],{"class":221,"line":349},6,[219,351,352],{"class":229},"  });\n",[219,354,356,359],{"class":221,"line":355},7,[219,357,358],{"class":225},"  return",[219,360,361],{"class":229}," (\n",[219,363,365,368,372,375,378,381,384,387,389,392,395,398,401,404,406,409,412,415,418,420,423],{"class":221,"line":364},8,[219,366,367],{"class":229},"    \u003C",[219,369,371],{"class":370},"sZBmE","div",[219,373,374],{"class":269}," ref",[219,376,377],{"class":225},"={",[219,379,380],{"class":229},"parentRef",[219,382,383],{"class":225},"}",[219,385,386],{"class":269}," style",[219,388,377],{"class":225},[219,390,391],{"class":229},"{ height: ",[219,393,394],{"class":236},"'70vh'",[219,396,397],{"class":229},", overflow: ",[219,399,400],{"class":236},"'auto'",[219,402,403],{"class":229}," }",[219,405,383],{"class":225},[219,407,408],{"class":269}," role",[219,410,411],{"class":225},"=",[219,413,414],{"class":236},"\"list\"",[219,416,417],{"class":269}," aria-label",[219,419,411],{"class":225},[219,421,422],{"class":236},"\"Orders\"",[219,424,425],{"class":229},">\n",[219,427,429,432,434,436,438,441,444,447,450,452,454],{"class":221,"line":428},9,[219,430,431],{"class":229},"      \u003C",[219,433,371],{"class":370},[219,435,386],{"class":269},[219,437,377],{"class":225},[219,439,440],{"class":229},"{ height: virtualizer.",[219,442,443],{"class":249},"getTotalSize",[219,445,446],{"class":229},"(), position: ",[219,448,449],{"class":236},"'relative'",[219,451,403],{"class":229},[219,453,383],{"class":225},[219,455,425],{"class":229},[219,457,459,462,465,468,471,474,477,480,483,485],{"class":221,"line":458},10,[219,460,461],{"class":225},"        {",[219,463,464],{"class":229},"virtualizer.",[219,466,467],{"class":249},"getVirtualItems",[219,469,470],{"class":229},"().",[219,472,473],{"class":249},"map",[219,475,476],{"class":229},"((",[219,478,479],{"class":256},"v",[219,481,482],{"class":229},") ",[219,484,325],{"class":225},[219,486,361],{"class":229},[219,488,490,493,495,498,500,503,505,507,509,512,515,517,520,522,524,527,529,532,535,538],{"class":221,"line":489},11,[219,491,492],{"class":229},"          \u003C",[219,494,371],{"class":370},[219,496,497],{"class":269}," key",[219,499,377],{"class":225},[219,501,502],{"class":229},"v.key",[219,504,383],{"class":225},[219,506,408],{"class":269},[219,508,411],{"class":225},[219,510,511],{"class":236},"\"listitem\"",[219,513,514],{"class":269}," aria-setsize",[219,516,377],{"class":225},[219,518,519],{"class":229},"rows.",[219,521,313],{"class":269},[219,523,383],{"class":225},[219,525,526],{"class":269}," aria-posinset",[219,528,377],{"class":225},[219,530,531],{"class":229},"v.index ",[219,533,534],{"class":225},"+",[219,536,537],{"class":269}," 1",[219,539,540],{"class":225},"}\n",[219,542,544,547,549,552,555,558,560,563,565,568,570,573,576,579,582,584,587,590,593,595,597],{"class":221,"line":543},12,[219,545,546],{"class":269},"               style",[219,548,377],{"class":225},[219,550,551],{"class":229},"{ position: ",[219,553,554],{"class":236},"'absolute'",[219,556,557],{"class":229},", top: ",[219,559,48],{"class":269},[219,561,562],{"class":229},", left: ",[219,564,48],{"class":269},[219,566,567],{"class":229},", right: ",[219,569,48],{"class":269},[219,571,572],{"class":229},", height: ",[219,574,575],{"class":269},"48",[219,577,578],{"class":229},", transform: ",[219,580,581],{"class":236},"`translateY(${",[219,583,479],{"class":229},[219,585,586],{"class":236},".",[219,588,589],{"class":229},"start",[219,591,592],{"class":236},"}px)`",[219,594,403],{"class":229},[219,596,383],{"class":225},[219,598,425],{"class":229},[219,600,602,605,608,611,613,616,618],{"class":221,"line":601},13,[219,603,604],{"class":229},"            \u003C",[219,606,607],{"class":370},"OrderRow",[219,609,610],{"class":269}," order",[219,612,377],{"class":225},[219,614,615],{"class":229},"rows[v.index]",[219,617,383],{"class":225},[219,619,620],{"class":229}," \u002F>\n",[219,622,624,627,629],{"class":221,"line":623},14,[219,625,626],{"class":229},"          \u003C\u002F",[219,628,371],{"class":370},[219,630,425],{"class":229},[219,632,634,637],{"class":221,"line":633},15,[219,635,636],{"class":229},"        ))",[219,638,540],{"class":225},[219,640,642,645,647],{"class":221,"line":641},16,[219,643,644],{"class":229},"      \u003C\u002F",[219,646,371],{"class":370},[219,648,425],{"class":229},[219,650,652,655,657],{"class":221,"line":651},17,[219,653,654],{"class":229},"    \u003C\u002F",[219,656,371],{"class":370},[219,658,425],{"class":229},[219,660,662],{"class":221,"line":661},18,[219,663,664],{"class":229},"  );\n",[219,666,668],{"class":221,"line":667},19,[219,669,540],{"class":229},[219,671,673],{"class":221,"line":672},20,[219,674,676],{"class":675},"sjfSM","\u002F\u002F trade-off: overscan of 10 rows hides blank areas on fast scrolls at the cost of\n",[219,678,680],{"class":221,"line":679},21,[219,681,682],{"class":675},"\u002F\u002F rendering ~20 extra rows; lower it on slow devices if rendering is expensive.\n",[204,684,686],{"id":685},"_2-handle-dynamic-heights-with-measurement","2. Handle dynamic heights with measurement",[209,688,690],{"className":211,"code":689,"language":213,"meta":214,"style":214},"const virtualizer = useVirtualizer({\n  count: rows.length, getScrollElement: () => parentRef.current,\n  estimateSize: () => 80,                                 \u002F\u002F reasonable estimate\n  measureElement: (el) => el.getBoundingClientRect().height,\n});\n\u002F\u002F In the row: \u003Cdiv data-index={v.index} ref={virtualizer.measureElement} ...>\n\u002F\u002F trade-off: measuring rows reads layout; libraries batch measurements, but very\n\u002F\u002F complex rows can still cause visible size corrections while scrolling.\n",[216,691,692,705,723,741,765,770,775,780],{"__ignoreMap":214},[219,693,694,697,699,701,703],{"class":221,"line":222},[219,695,696],{"class":225},"const",[219,698,296],{"class":269},[219,700,273],{"class":225},[219,702,301],{"class":249},[219,704,304],{"class":229},[219,706,707,710,712,714,716,718,720],{"class":221,"line":243},[219,708,709],{"class":229},"  count: rows.",[219,711,313],{"class":269},[219,713,316],{"class":229},[219,715,319],{"class":249},[219,717,322],{"class":229},[219,719,325],{"class":225},[219,721,722],{"class":229}," parentRef.current,\n",[219,724,725,728,730,732,735,738],{"class":221,"line":263},[219,726,727],{"class":249},"  estimateSize",[219,729,322],{"class":229},[219,731,325],{"class":225},[219,733,734],{"class":269}," 80",[219,736,737],{"class":229},",                                 ",[219,739,740],{"class":675},"\u002F\u002F reasonable estimate\n",[219,742,743,746,749,752,754,756,759,762],{"class":221,"line":291},[219,744,745],{"class":249},"  measureElement",[219,747,748],{"class":229},": (",[219,750,751],{"class":256},"el",[219,753,482],{"class":229},[219,755,325],{"class":225},[219,757,758],{"class":229}," el.",[219,760,761],{"class":249},"getBoundingClientRect",[219,763,764],{"class":229},"().height,\n",[219,766,767],{"class":221,"line":307},[219,768,769],{"class":229},"});\n",[219,771,772],{"class":221,"line":349},[219,773,774],{"class":675},"\u002F\u002F In the row: \u003Cdiv data-index={v.index} ref={virtualizer.measureElement} ...>\n",[219,776,777],{"class":221,"line":355},[219,778,779],{"class":675},"\u002F\u002F trade-off: measuring rows reads layout; libraries batch measurements, but very\n",[219,781,782],{"class":221,"line":364},[219,783,784],{"class":675},"\u002F\u002F complex rows can still cause visible size corrections while scrolling.\n",[204,786,788],{"id":787},"_3-memoize-rows","3. Memoize rows",[15,790,791,792,795],{},"Wrap row components in ",[216,793,794],{},"React.memo"," and pass stable props, so scrolling and parent updates only render rows whose data changed.",[204,797,799],{"id":798},"_4-window-level-scrolling-for-page-lists","4. Window-level scrolling for page lists",[15,801,802,803,806],{},"For feeds that scroll with the page rather than a container, use the library's window virtualizer (",[216,804,805],{},"useWindowVirtualizer",") so the list integrates with normal page scrolling.",[15,808,809],{},[35,810,42,813,42,816,42,819,42,821,42,824,42,826,42,833,42,839,42,844,42,848,42,852,42,855,42,858,42,862,42,865,42,869,42,873,42,876,42,879,42,883,42],{"viewBox":811,"width":38,"role":39,"ariaLabel":812,"style":41},"0 0 760 318","The layers of a virtualized list from the scroll container down to the rendered rows.",[44,814],{"className":815,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],[51,817,818],{},"Anatomy of a virtualized list",[55,820,812],{},[44,822],{"x":59,"y":59,"width":60,"height":823,"rx":62,"fill":63,"stroke":64,"style":65},"316",[67,825,818],{"x":69,"y":70,"fill":64,"style":71},[44,827],{"x":69,"y":82,"width":828,"height":829,"rx":830,"fill":831,"stroke":831,"style":832},"704.0","40.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[67,834,838],{"x":835,"y":836,"fill":64,"style":837,"textAnchor":589},"42.0","80.5","font-size:13px;font-weight:700","Scroll container",[67,840,843],{"x":841,"y":836,"fill":64,"style":842,"textAnchor":589},"191.8","font-size:12px","Fixed height, overflow auto (or the window)",[44,845],{"x":69,"y":846,"width":828,"height":829,"rx":830,"fill":64,"stroke":64,"style":847},"106.0","fill-opacity:0.06;stroke-opacity:0.4",[67,849,851],{"x":835,"y":850,"fill":64,"style":837,"textAnchor":589},"130.5","Spacer",[67,853,854],{"x":841,"y":850,"fill":64,"style":842,"textAnchor":589},"Height equals total list height",[44,856],{"x":69,"y":857,"width":828,"height":829,"rx":830,"fill":64,"stroke":64,"style":847},"156.0",[67,859,861],{"x":835,"y":860,"fill":64,"style":837,"textAnchor":589},"180.5","Overscan rows above",[67,863,864],{"x":841,"y":860,"fill":64,"style":842,"textAnchor":589},"Rendered but offscreen",[44,866],{"x":69,"y":867,"width":828,"height":829,"rx":830,"fill":112,"stroke":113,"style":868},"206.0","fill-opacity:0.24;stroke-opacity:0.9",[67,870,872],{"x":835,"y":871,"fill":64,"style":837,"textAnchor":589},"230.5","Visible rows",[67,874,875],{"x":841,"y":871,"fill":64,"style":842,"textAnchor":589},"Absolutely positioned with translateY",[44,877],{"x":69,"y":878,"width":828,"height":829,"rx":830,"fill":64,"stroke":64,"style":847},"256.0",[67,880,882],{"x":835,"y":881,"fill":64,"style":837,"textAnchor":589},"280.5","Overscan rows below",[67,884,864],{"x":841,"y":881,"fill":64,"style":842,"textAnchor":589},[138,886,888],{"id":887},"verification","Verification",[15,890,891],{},"Check that DOM row count stays constant while scrolling (roughly visible rows plus overscan). Profile initial render and common interactions; both should no longer scale with list length. Scroll quickly on a throttled device and confirm no prolonged blank areas. Test keyboard navigation and a screen reader for list semantics.",[138,893,895],{"id":894},"worked-example-a-logistics-dashboard","Worked Example: A Logistics Dashboard",[15,897,898],{},"A logistics company's dashboard showed up to 8,000 shipments in a table. Rendering took 2.3 seconds on the operations team's laptops, and selecting a row took 400ms because the selection state lived in the parent and every row re-rendered. The team virtualized the table with TanStack Virtual (fixed 44px rows, overscan 12), memoized rows with selection passed as a boolean prop, and moved filtering to a Web Worker. Initial render dropped to 90ms, row selection to 16ms, and INP p75 for the page from 420ms to 110ms. They added a search box and keyboard shortcuts to make up for find-in-page no longer finding offscreen rows.",[15,900,901],{},[35,902,42,905,42,908,42,911,42,913,42,930,42,933,42,935,42,939,42,945,42,948,42,952,42,960,42,966,42,970,42,975,42,978,42,981,42,983,42,986,42,989,42,992,42,995,42,998,42,1000,42,1004,42,1006,42,1009,42,1012,42,1015,42,1019,42,1022,42,1025,42],{"viewBox":903,"width":38,"role":39,"ariaLabel":904,"style":41},"0 0 760 325","Decision sequence for choosing a technique for a long list.",[44,906],{"className":907,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],[51,909,910],{},"Virtualize, paginate or content-visibility?",[55,912,904],{},[914,915,916],"defs",{},[917,918,925],"marker",{"id":919,"viewBox":920,"refX":921,"refY":922,"markerWidth":923,"markerHeight":923,"orient":924},"fad1a90476","0 0 10 10","9","5","7","auto-start-reverse",[926,927],"path",{"d":928,"fill":64,"style":929},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[44,931],{"x":59,"y":59,"width":60,"height":932,"rx":62,"fill":63,"stroke":64,"style":65},"323",[67,934,910],{"x":69,"y":70,"fill":64,"style":71},[44,936],{"x":69,"y":82,"width":937,"height":938,"rx":830,"fill":64,"stroke":64,"style":847},"320.0","35.0",[67,940,944],{"x":941,"y":942,"fill":64,"style":837,"textAnchor":943},"188.0","78.0","middle","Must every row be indexable or printable?",[44,946],{"x":947,"y":82,"width":937,"height":938,"rx":830,"fill":831,"stroke":831,"style":832},"412.0",[67,949,951],{"x":950,"y":942,"fill":64,"style":135,"textAnchor":943},"572.0","Paginate with server rendering",[221,953],{"x1":954,"y1":955,"x2":956,"y2":955,"stroke":64,"strokeWidth":957,"style":958,"markerEnd":959},"348.0","73.5","410.0","1.5","stroke-opacity:0.6","url(#fad1a90476)",[67,961,965],{"x":962,"y":963,"fill":831,"style":964,"textAnchor":943},"380.0","66.5","font-size:11.5px;font-weight:700","yes",[221,967],{"x1":941,"y1":968,"x2":941,"y2":969,"stroke":64,"strokeWidth":957,"style":958,"markerEnd":959},"91.0","125.0",[67,971,974],{"x":972,"y":973,"fill":134,"style":964},"196.0","113.0","no",[44,976],{"x":69,"y":977,"width":937,"height":938,"rx":830,"fill":64,"stroke":64,"style":847},"127.0",[67,979,980],{"x":941,"y":125,"fill":64,"style":837,"textAnchor":943},"Fewer than a few hundred rows?",[44,982],{"x":947,"y":977,"width":937,"height":938,"rx":830,"fill":831,"stroke":831,"style":832},[67,984,985],{"x":950,"y":125,"fill":64,"style":135,"textAnchor":943},"content-visibility auto on row groups",[221,987],{"x1":954,"y1":988,"x2":956,"y2":988,"stroke":64,"strokeWidth":957,"style":958,"markerEnd":959},"144.5",[67,990,965],{"x":962,"y":991,"fill":831,"style":964,"textAnchor":943},"137.5",[221,993],{"x1":941,"y1":994,"x2":941,"y2":972,"stroke":64,"strokeWidth":957,"style":958,"markerEnd":959},"162.0",[67,996,974],{"x":972,"y":997,"fill":134,"style":964},"184.0",[44,999],{"x":69,"y":133,"width":937,"height":938,"rx":830,"fill":64,"stroke":64,"style":847},[67,1001,1003],{"x":941,"y":1002,"fill":64,"style":837,"textAnchor":943},"220.0","Thousands of rows or unbounded growth?",[44,1005],{"x":947,"y":133,"width":937,"height":938,"rx":830,"fill":112,"stroke":113,"style":868},[67,1007,1008],{"x":950,"y":1002,"fill":64,"style":135,"textAnchor":943},"Virtualize",[221,1010],{"x1":954,"y1":1011,"x2":956,"y2":1011,"stroke":64,"strokeWidth":957,"style":958,"markerEnd":959},"215.5",[67,1013,965],{"x":962,"y":1014,"fill":831,"style":964,"textAnchor":943},"208.5",[221,1016],{"x1":941,"y1":1017,"x2":941,"y2":1018,"stroke":64,"strokeWidth":957,"style":958,"markerEnd":959},"233.0","267.0",[67,1020,974],{"x":972,"y":1021,"fill":134,"style":964},"255.0",[44,1023],{"x":69,"y":1024,"width":937,"height":70,"rx":830,"fill":112,"stroke":113,"style":868},"269.0",[67,1026,1028],{"x":941,"y":1027,"fill":64,"style":135,"textAnchor":943},"290.5","Paginate or load more in batches",[138,1030,1032],{"id":1031},"keeping-virtualized-lists-usable","Keeping Virtualized Lists Usable",[15,1034,1035,1036,1039,1040,1043,1044,1047,1048,1051,1052,1055,1056,1059,1060,1063,1064,1067,1068,1063,1071,1074],{},"Virtualization changes browser behaviour users rely on. ",[149,1037,1038],{},"Find-in-page"," cannot find rows that are not rendered; provide an in-app search or filter. ",[149,1041,1042],{},"Scroll restoration"," after navigation needs the list to restore its offset once data is loaded; store the scroll offset or the first visible index and restore it after the virtualizer mounts. ",[149,1045,1046],{},"Anchor links"," to specific rows need ",[216,1049,1050],{},"scrollToIndex",". ",[149,1053,1054],{},"Printing"," prints only rendered rows; provide a print view or export. ",[149,1057,1058],{},"Screen readers"," need ",[216,1061,1062],{},"aria-setsize"," and ",[216,1065,1066],{},"aria-posinset"," (or ",[216,1069,1070],{},"aria-rowcount",[216,1072,1073],{},"aria-rowindex"," for grids) so they announce the full list size.",[138,1076,1078],{"id":1077},"common-mistakes","Common Mistakes",[143,1080,1081,1087,1093,1099],{},[146,1082,1083,1086],{},[149,1084,1085],{},"Virtualizing short lists."," Under a few hundred rows, the complexity rarely pays off.",[146,1088,1089,1092],{},[149,1090,1091],{},"Unmemoized rows."," Scrolling re-renders every visible row on each update.",[146,1094,1095,1098],{},[149,1096,1097],{},"Wrong size estimates."," Scrollbar jumps and content shifting during scroll.",[146,1100,1101,1104],{},[149,1102,1103],{},"Ignoring accessibility."," Screen readers announce only the rendered items.",[138,1106,1108],{"id":1107},"edge-cases","Edge Cases",[15,1110,1111,1114],{},[149,1112,1113],{},"Sticky headers in tables."," Render headers outside the virtualized body, or use the library's sticky support.",[15,1116,1117,1120],{},[149,1118,1119],{},"Grids (2D virtualization)."," Virtualize rows and columns for very wide data grids; libraries support both axes.",[15,1122,1123,1126],{},[149,1124,1125],{},"Infinite loading."," Combine virtualization with data fetching when the last rows come into view.",[15,1128,1129,1132],{},[149,1130,1131],{},"Server rendering."," Render the first window of rows on the server for SEO and fast first paint; the virtualizer takes over on hydration.",[138,1134,1136],{"id":1135},"faq","FAQ",[1138,1139,1142,1146],"details",{"className":1140},[1141],"faq-item",[1143,1144,1145],"summary",{},"Which React virtualization library should I use?",[15,1147,1148],{},"TanStack Virtual is headless and flexible; react-window is small and simple for fixed sizes; react-virtuoso handles dynamic heights and grouped lists with less setup. Choose based on layout needs.",[1138,1150,1152,1155],{"className":1151},[1141],[1143,1153,1154],{},"How many rows justify virtualization?",[15,1156,1157],{},"As a rule of thumb, several hundred rows of moderate complexity, or any list that can grow unbounded. Measure render time and interaction cost first.",[1138,1159,1161,1164],{"className":1160},[1141],[1143,1162,1163],{},"Does virtualization work with CSS grid layouts?",[15,1165,1166],{},"Yes, by virtualizing rows of grid items, with each virtual row rendering a fixed number of columns based on container width.",[1138,1168,1170,1173],{"className":1169},[1141],[1143,1171,1172],{},"Why do I see blank areas when scrolling fast?",[15,1174,1175],{},"Rows render after scroll events; on slow devices they can lag. Increase overscan slightly and keep row components cheap to render.",[1138,1177,1179,1182],{"className":1178},[1141],[1143,1180,1181],{},"Does virtualization reduce memory use?",[15,1183,1184],{},"Yes, significantly — only rendered rows exist as components and DOM nodes. Data for all rows still lives in memory.",[1138,1186,1188,1191],{"className":1187},[1141],[1143,1189,1190],{},"Can I use content-visibility instead?",[15,1192,1193,1194,1197],{},"For lists up to a few hundred rows, ",[216,1195,1196],{},"content-visibility: auto"," on row groups gives much of the rendering benefit while keeping all rows in the DOM. For thousands of rows, virtualization is more effective.",[1138,1199,1201,1204],{"className":1200},[1141],[1143,1202,1203],{},"How do I keep keyboard navigation working?",[15,1205,1206,1207,1209],{},"Handle arrow keys at the list level, track the active index in state, and call ",[216,1208,1050],{}," to bring the active row into view before focusing it.",[1138,1211,1213,1216],{"className":1212},[1141],[1143,1214,1215],{},"Is virtualization compatible with React Server Components?",[15,1217,1218],{},"The virtualized list is a client component because it depends on scroll position. Server components can fetch data and pass it to the client list.",[1138,1220,1222,1225],{"className":1221},[1141],[1143,1223,1224],{},"Should filtering happen before or inside the virtualized list?",[15,1226,1227],{},"Before. Filter and sort the data array (in a memoized selector or a Web Worker for large datasets), then pass the result to the virtualizer. The virtualizer only needs the final row count and items.",[1138,1229,1231,1234],{"className":1230},[1141],[1143,1232,1233],{},"Can I animate rows in a virtualized list?",[15,1235,1236],{},"Keep animations to transform and opacity on rendered rows. Layout animations across many rows defeat the purpose and can confuse the virtualizer’s measurements.",[138,1238,1240],{"id":1239},"related","Related",[143,1242,1243,1250,1257],{},[146,1244,1245,1249],{},[19,1246,1248],{"href":1247},"\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Ffixing-the-excessive-dom-size-warning\u002F","Fixing the excessive DOM size warning"," — other ways to shrink the DOM.",[146,1251,1252,1256],{},[19,1253,1255],{"href":1254},"\u002Frendering-css-performance\u002Fcss-containment-and-content-visibility\u002Fusing-content-visibility-auto-on-long-pages\u002F","Using content-visibility: auto on long pages"," — the lighter alternative.",[146,1258,1259,1263],{},[19,1260,1262],{"href":1261},"\u002Fframework-performance\u002Freact-rendering-performance\u002F","React performance"," — memoization and rendering costs.",[1265,1266,1268],"script",{"type":1267},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Virtualize Long Lists in React\",\n  \"description\": \"How to virtualize long lists and tables in React, handle variable row heights, and keep virtualized lists accessible and navigable.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Virtualize with fixed row heights\",\n      \"text\": \"Virtualize with fixed row heights\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Handle dynamic heights with measurement\",\n      \"text\": \"Handle dynamic heights with measurement\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Memoize rows\",\n      \"text\": \"Wrap row components in React.memo and pass stable props, so scrolling and parent updates only render rows whose data changed.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Window-level scrolling for page lists\",\n      \"text\": \"For feeds that scroll with the page rather than a container, use the library's window virtualizer (useWindowVirtualizer) so the list integrates with normal page scrolling.\"\n    }\n  ]\n}\n",[1265,1270,1271],{"type":1267},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Virtualize Long Lists in React\",\n  \"description\": \"How to virtualize long lists and tables in React, handle variable row heights, and keep virtualized lists accessible and navigable.\",\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\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react\u002F\"\n  }\n}\n",[1265,1273,1274],{"type":1267},"\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\": \"Rendering & CSS Performance\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"DOM Size & List Virtualization\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Virtualizing Long Lists in React\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react\u002F\"\n    }\n  ]\n}\n",[1276,1277,1278],"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 .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}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);}",{"title":214,"searchDepth":243,"depth":243,"links":1280},[1281,1282,1283,1289,1290,1291,1292,1293,1294,1295],{"id":140,"depth":243,"text":141},{"id":173,"depth":243,"text":174},{"id":201,"depth":243,"text":202,"children":1284},[1285,1286,1287,1288],{"id":206,"depth":263,"text":207},{"id":685,"depth":263,"text":686},{"id":787,"depth":263,"text":788},{"id":798,"depth":263,"text":799},{"id":887,"depth":243,"text":888},{"id":894,"depth":243,"text":895},{"id":1031,"depth":243,"text":1032},{"id":1077,"depth":243,"text":1078},{"id":1107,"depth":243,"text":1108},{"id":1135,"depth":243,"text":1136},{"id":1239,"depth":243,"text":1240},"How to virtualize long lists and tables in React, handle variable row heights, and keep virtualized lists accessible and navigable.","md",{"slug":1299,"type":1300,"breadcrumb":1301,"datePublished":1309,"dateModified":1309},"virtualizing-long-lists-in-react","article",[1302,1305,1306,1307],{"name":1303,"url":1304},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1308},"\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react\u002F","2026-10-06",true,"\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react",{"title":1313,"description":1314},"Virtualizing Long Lists in React with TanStack Virtual","Render only visible rows of long React lists with TanStack Virtual or react-window: fixed and dynamic heights, overscan, accessibility and scroll restoration.","rendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react\u002Findex","WL1tzZ1fI5AhC0PsxJM4Vf7NhOLGwwYFD6wRUuAQYhY",[1318,1322],{"title":1319,"path":1320,"stem":1321,"children":-1},"Reducing Style Recalculation Cost","\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Freducing-style-recalculation-cost","rendering-css-performance\u002Fdom-size-and-list-virtualization\u002Freducing-style-recalculation-cost\u002Findex",{"title":1323,"path":1324,"stem":1325,"children":-1},"Layout Thrashing & Forced Reflow","\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow","rendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Findex",1791308072345]