Suggest an editImprove this articleRefine the answer for “What is virtualization and why is it needed”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Virtualization** renders only the DOM nodes visible in the viewport, recycling ~20-50 elements as the user scrolls, instead of creating one node per list item. ```jsx import { FixedSizeList as List } from 'react-window'; <List height={400} itemCount={10000} itemSize={35}> {({ index, style }) => <div style={style}>Item {index}</div>} </List> // ~20 DOM nodes at any scroll position, not 10,000 ``` **Key point:** use when a list exceeds 500 items and scroll starts to lag.Shown above the full answer for quick recall.Answer (EN)Image**Virtualization** renders only the DOM nodes visible in the viewport, recycling a fixed pool of ~20-50 elements as the user scrolls, regardless of how many items the list actually contains. ## Theory ### TL;DR - Analogy: a theater with 20 seats where actors swap through side doors as the spotlight moves. You never build 10,000 seats. - Full render: 10,000 DOM nodes for 10,000 items. Virtualized: 20-50 nodes, reused on scroll. - Memory drops from 100MB+ to ~2MB for the same dataset. - Initial load goes from 2s+ to ~100ms. - Use it when a scrollable list exceeds 500 items and scroll lags; skip it for short static lists. ### Quick Example ```jsx // Without virtualization: 10k DOM nodes, browser struggles on scroll const SlowList = ({ items }) => ( <ul style={{ height: '400px', overflow: 'auto' }}> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> ); // With react-window: ~20 nodes regardless of items.length import { FixedSizeList as List } from 'react-window'; const FastList = ({ items }) => ( <List height={400} itemCount={items.length} itemSize={35}> {({ index, style }) => <div style={style}>{items[index].name}</div>} </List> ); // Open DevTools during scroll: same ~20 divs repositioned, not new ones added ``` The library positions each visible row with `transform: translateY()`, so the browser skips reflow entirely and holds 60fps through the whole scroll. ### How the DOM Pool Works The browser figures out which rows fit the viewport using `getBoundingClientRect()` or `ResizeObserver`. The library keeps a small buffer (called overscan) of extra rows above and below the visible area, typically 3-5 rows. When the user scrolls, existing DOM nodes get new content and a new `translateY` offset. No nodes are created or destroyed mid-scroll. React reconciles via the `key` on each virtual item. Skip the key and React re-renders every row on every scroll event. That one omission drops frame rate from 60fps to 10fps, and it shows up in code reviews more often than you'd expect. ### When to Use - List or table with more than 500 items -> virtualize. - Infinite scroll feed (email inbox, social timeline) -> load chunks and virtualize the visible slice. - Nested tree structures like a file explorer -> virtualize only expanded branches. - Mobile app with a scrollable list over 200 items -> always (memory is tighter on phones). - Static list under 200 items -> skip it. The library adds ~10kb for no measurable gain. ### Comparison Table | | Full Render | Virtualized | |---|---|---| | DOM nodes (10k items) | 10,000 | 20-50, reused | | Scroll at 60fps | No | Yes | | Memory (10k items) | 100MB+ | ~2MB | | Initial load | 2s+ | ~100ms | | Best for | <500 static items | >500 dynamic or scrollable lists | ### Common Mistakes **Missing `key` on virtual items.** React needs a stable key to track which item maps to which DOM node. The list looks fine in development, but drops to 10fps in production when data updates often. ```jsx // Wrong - triggers full re-render on every scroll event {virtualItems.map(item => <div>{data[item.index]}</div>)} // Right {virtualItems.map(item => <div key={item.key}>{data[item.index]}</div>)} ``` **Zero overscan on fast-scrolling lists.** Flicking the list produces white gaps where rows should be. ```jsx // Wrong - user sees blank space during fast fling const virtualizer = useVirtualizer({ count: items.length, overscan: 0 }); // Right - render 5 extra rows above and below the viewport const virtualizer = useVirtualizer({ count: items.length, overscan: 5 }); ``` **Fixed row height for variable-height content.** Chat messages with images break layout when every row is assumed to be 50px. ```jsx // Wrong - gaps and misaligned rows with emojis or images const virtualizer = useVirtualizer({ estimateSize: () => 50 }); // Right - measure real heights at runtime const virtualizer = useVirtualizer({ count: messages.length, estimateSize: () => 50, measureElement: el => el?.getBoundingClientRect()?.height || 50, }); ``` **Virtualizing small lists.** Adding `react-window` for 150 items ships 10kb of JS for no gain. Use `.map()`. **Nested virtualization.** A virtual list inside a virtual table causes double `translateY` transforms that overflow and break positioning. Flatten the structure or use `position: sticky` for headers instead. ### Real-World Usage - Jira and Gmail web use virtualization for issue lists and inbox with 10,000+ rows. - TanStack Table v8+ ships with virtual row support via `@tanstack/react-virtual`. - Ant Design Table uses `vc-virtual-list` internally for enterprise dashboards. - Vercel admin panels virtualize log output with infinite scroll. - For new projects, prefer `@tanstack/react-virtual` (actively maintained, ~5kb) over `react-virtualized` (legacy, heavier). ### Follow-up Questions **Q:** What is the difference between virtualization and windowing? **A:** Same concept, different name. "Windowing" was the older term from `react-virtualized`; the community now uses "virtualization". **Q:** How does `react-window` handle variable row heights? **A:** Use `VariableSizeList` and pass a `getItemSize(index)` function. It caches each measured height and recalculates scroll offsets when the container resizes. **Q:** How does React 18 affect virtualization performance? **A:** `startTransition` wraps scroll handler updates so they run as non-urgent transitions. Scroll jank disappears even when rows contain expensive components, because React can interrupt the update for higher-priority input events. **Q:** Why not use CSS `contain: layout` instead of removing DOM nodes? **A:** `contain` tells the browser to skip style recalculations outside the container, but the nodes stay in the DOM. For 50,000 rows, memory is still blown. Virtualization removes the nodes entirely. **Q (senior):** `ResizeObserver` breaks virtualization in Safari 15-16. How do you fix it without switching libraries? **A:** Polyfill with `MutationObserver` on the scroll container, throttle the `scroll` event handler to 16ms intervals, and add `window.resize` as a fallback. This specific bug comes up in FAANG mobile interviews. ## Examples ### Basic: Fixed Row Height with react-window ```jsx import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}>User #{index}</div> ); function UserList({ users }) { return ( <List height={400} itemCount={users.length} itemSize={35} width="100%"> {Row} </List> ); } // DOM holds ~20 rows at any scroll position ``` Each row is 35px tall. The `itemSize` prop must match the actual CSS height or scroll position drifts. Open DevTools during scroll and you will see the same ~20 divs getting repositioned, not destroyed and recreated. ### Production: Dynamic List with TanStack Virtual ```jsx import { useRef } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; function IssueList({ issues }) { // e.g. 50k issues from API const parentRef = useRef(); const virtualizer = useVirtualizer({ count: issues.length, getScrollElement: () => parentRef.current, estimateSize: () => 40, overscan: 5, }); return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}> {virtualizer.getVirtualItems().map(item => ( <div key={item.key} style={{ position: 'absolute', top: 0, transform: `translateY(${item.start}px)`, width: '100%', }} > {issues[item.index].title} </div> ))} </div> </div> ); } // ~30 rows in DOM while scrolling through 50k issues ``` The outer `div` gets the full virtual height so the scrollbar accurately reflects list size. Each visible row is positioned with `translateY`, which skips layout recalculation. This is the same pattern TanStack Table v8 and Shadcn dashboards use in production.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.