Problem Statement
Render a list of 1000 items in a fixed size scroll box, but only put the rows that are on screen into the DOM.
Functional Requirements
- Props:
VirtualizedListtakeslist,height,widthanditemHeight. The demo uses 1000 items, a 400 by 300 box and 35px rows. - Full scroll height: The inner div is
list.length * itemHeighttall, so the scrollbar matches the full list. - Visible window: On scroll, compute the start index from
scrollTopand render only the rows from start to start plus the rows that fit inheight. - Row position: Each rendered row is absolutely positioned at
index * itemHeight, so it lands where it would sit in the full list.
Technical Constraints
- All rows have the same fixed height. Variable row heights are not supported.
- There is no overscan buffer above or below the visible rows.
What is Virtualization?
Virtualization is a technique used to render only the visible portion of a large list of items, improving performance by reducing the number of DOM elements rendered at any given time. This is particularly useful when dealing with long lists or infinite scrolling implementations.
When to Use Virtualization
Use virtualization when:
- Rendering large lists (hundreds or thousands of items)
- Dealing with infinite scroll implementations
- Performance optimization is needed
- Memory usage needs to be reduced
- Smooth scrolling experience is required
Code Breakdown
Let's break down the implementation step by step.
Component Props
function VirtualizedList({ list, height, width, itemHeight })list: Array of items to be renderedheight: Container heightwidth: Container widthitemHeight: Height of each list item
State Management
const [indicies, setIndicies] = useState([0, Math.ceil(height / itemHeight)]);- Maintains visible items' start and end indices
- Initial state:
- Start index: 0
- End index: Number of items that fit in the visible area, rounded up so a partly visible last row is still drawn
Visible Items Calculation
const visibleList = list.slice(indicies[0], indicies[1] + 1);- Creates a subset of the list containing only visible items
- Uses array slice from start index to end index + 1
Scroll Handler
const handleScroll = (e) => {
const { scrollTop } = e.target;
const newStartIndex = Math.floor(scrollTop / itemHeight);
const newEndIndex = newStartIndex + Math.ceil(height / itemHeight);
setIndicies([newStartIndex, newEndIndex]);
};- Gets scroll position (
scrollTop) - Calculates new start index based on scroll position
- Calculates new end index based on container height
- Updates indices state
Render Logic
return (
<div
className="container"
style={{
height,
width,
backgroundColor: "lightgray",
overflow: "auto",
}}
onScroll={handleScroll}
>- Creates scrollable container with specified dimensions
- Attaches scroll event handler
Item Container
<div style={{ height: list?.length * itemHeight, position: "relative" }}>- Sets total height based on all items
- Uses relative positioning for absolute positioning of children
Item Rendering
{visibleList.map((item, index) => (
<div
className="item"
key={index}
style={{
height: itemHeight,
background: "coral",
borderTop: "5px solid grey",
position: "absolute",
top: (indicies[0] + index) * itemHeight,
width: "100%",
textAlign: "center",
}}
>
{"Item " + item}
</div>
))}- Maps through visible items only
- Positions each item absolutely based on its index
- Calculates top position using: (startIndex + currentIndex) * itemHeight
Example Run-through
Let's say we have these parameters:
- Container height: 400px
- Item height: 50px
- Total items: 1000
Initial render:
- Visible items = 400/50 = 8 items
- Initial indices: [0, 8]
- Only items 0-8 are rendered
When scrolling down 100px:
- New start index = 100/50 = 2
- New end index = 2 + 8 = 10
- Updates indices to [2, 10]
- Re-renders only items 2-10
This way, even with 1000 items, we're only rendering 8-9 items at any given time, significantly improving performance.