
Virtual Scrolling: Taming the Million-Row Beast Without Crushing the Browser
Rendering massive datasets in the browser usually means a sluggish UI, memory hogs, and frustrated users. Virtual scrolling is the answer, but getting it right is harder than it looks. Let's dive into how to build a robust virtual scroller that handles millions of rows with ease.
by Sunil Band
The Million-Row Problem
We've all been there: a client wants to display all their data in a single table, and "all" means hundreds of thousands, sometimes millions, of rows. Your initial reaction is probably a mix of dread and resignation. You know that dumping that much DOM into the browser will bring it to its knees, leading to slow rendering, memory leaks, and a user experience so bad it's practically unusable. The browser wasn't designed to render an entire database table at once.
So what do you do? Pagination helps, but often users demand the ability to scroll through everything. Infinite scrolling is a step better, fetching more data as you approach the bottom, but it still eventually hits the same wall: too many DOM nodes. The real challenge is showing a subset of a massive list dynamically based on scroll position, without actually rendering the invisible parts.
This is where virtual scrolling comes in. It's the technique of rendering only the items currently visible within the viewport, plus a small buffer, while maintaining the illusion of a continuous, scrollable list. It's not magic, but it feels like it when you get it right. It allows you to display truly enormous datasets without breaking the browser or your users' patience.
How Virtual Scrolling Works: The Core Idea
The fundamental principle behind virtual scrolling is simple: decoupling the scrollable height from the actual rendered content. Instead of rendering all N items and letting the browser calculate the scroll height, we:
- Calculate the total theoretical height of all
Nitems as if they were all rendered. - Create a container element with this calculated height to act as our scrollable canvas.
- Render only a small window of items that are currently visible within the viewport.
- Dynamically adjust the
transform(ortopstyle) of the rendered items' container to simulate their correct position within the full scrollable space.
This means that no matter if you have 100 items or 10 million, you only ever have a few dozen (or a few hundred, depending on buffer) actual DOM elements in memory. The performance gains are immediate and dramatic.
The Math Behind the Illusion
Let's break down the key pieces of information we need to manage:
-
itemHeight: The height of a single item. For simplicity, we'll assume fixed height items for now. Variable height items introduce more complexity, which we'll touch on later. -
totalItems: The total number of items in our dataset. -
viewportHeight: The height of the scrollable container. -
scrollTop: The current vertical scroll position of the container.
From these, we can calculate everything else:
-
totalScrollHeight = totalItems * itemHeight -
startIndex = floor(scrollTop / itemHeight) -
endIndex = ceil((scrollTop + viewportHeight) / itemHeight) -
renderedItems = items.slice(startIndex, endIndex + buffer)(we add a buffer to prevent flickering during fast scrolls) -
offset = startIndex * itemHeight(this is thetransformYortopfor our rendered window)
The offset is crucial. It shifts our small window of rendered items down the page, making it appear as if they are part of a much larger list. As the user scrolls, scrollTop changes, which updates startIndex, endIndex, and offset, causing a new window of items to be rendered and positioned.
Building a Basic Virtual Scroller in React
Let's put this into practice with a simplified React component. We'll use a div as our scrollable container and some state to manage the visible range.
import React, { useRef, useState, useEffect, useCallback } from 'react';
interface VirtualizedListProps<T> {
items: T[];
itemHeight: number; // For now, assume fixed height items
renderItem: (item: T, index: number) => React.ReactNode;
buffer?: number; // Number of items to render above/below viewport
}
function VirtualizedList<T>({ items, itemHeight, renderItem, buffer = 5 }: VirtualizedListProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const [viewportHeight, setViewportHeight] = useState(0);
const totalItems = items.length;
const totalScrollHeight = totalItems * itemHeight;
useEffect(() => {
// Initialize viewportHeight on mount
if (containerRef.current) {
setViewportHeight(containerRef.current.clientHeight);
}
// Add resize observer for responsive viewport height
const observer = new ResizeObserver(entries => {
if (entries[0]) {
setViewportHeight(entries[0].contentRect.height);
}
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
const handleScroll = useCallback(() => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop);
}
}, []);
useEffect(() => {
const container = containerRef.current;
if (container) {
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}
}, [handleScroll]);
// Calculate visible range
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
const endIndex = Math.min(totalItems - 1, Math.ceil((scrollTop + viewportHeight) / itemHeight) + buffer);
// Slice items to render and calculate offset
const visibleItems = items.slice(startIndex, endIndex + 1); // +1 because slice end is exclusive
const offset = startIndex * itemHeight;
return (
<div
ref={containerRef}
style={{
height: '100%', // Or any fixed height
overflowY: 'auto',
position: 'relative',
willChange: 'transform' // Performance hint for browser
}}
>
<div
style={{
height: totalScrollHeight, // This creates the scrollable space
position: 'relative'
}}
>
<div
style={{
transform: `translateY(${offset}px)`,
position: 'absolute',
top: 0,
left: 0,
right: 0,
willChange: 'transform' // Another performance hint
}}
>
{visibleItems.map((item, index) => renderItem(item, startIndex + index))}
</div>
</div>
</div>
);
}
export default VirtualizedList;
// Example Usage:
/*
function App() {
const data = Array.from({ length: 1000000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
const renderRow = (item: { id: number; name: string }) => (
<div key={item.id} style={{ height: 30, padding: 5, borderBottom: '1px solid #eee' }}>
{item.id}: {item.name}
</div>
);
return (
<div style={{ height: '600px', width: '400px', border: '1px solid #ccc' }}>
<VirtualizedList items={data} itemHeight={30} renderItem={renderRow} />
</div>
);
}
*/In this example:
-
containerRefpoints to our scrollablediv. We use aResizeObserverto react to changes in its height, ensuring our calculations stay accurate. -
scrollTopandviewportHeightare state variables that trigger re-renders when the scroll position or container size changes. -
totalScrollHeightsets the actual height of an innerdiv, which is what the browser uses to determine the total scrollable area. - The
transform: translateY(${offset}px)on thevisibleItemscontainer is the key. It moves our small window of rendered components to the correct visual position within thetotalScrollHeight. - The
bufferprop is important. Without it, items would pop in and out abruptly at the exact edge of the viewport. A small buffer makes scrolling feel much smoother.
Even with a million items in the data array, this component will only ever render a handful of divs, providing incredibly smooth performance.
Variable Item Heights: The Next Frontier
Fixed item height is a nice simplification, but real-world data often comes with variable heights. Think multiline text, images, or dynamically sized components. Handling this efficiently is the real trick in advanced virtual scrolling.
When item heights vary, our simple startIndex = floor(scrollTop / itemHeight) calculation breaks. We can no longer just multiply an index by a fixed height to get an offset. Instead, we need to know the cumulative height up to each item. This usually means:
- Measuring items after they render: This is problematic because we're trying to render before we know the height. It can lead to scroll jumps.
- Estimating heights: Provide an average
estimatedItemHeightand update actual heights as items enter the viewport. This means the scrollbar might jump a bit initially but stabilizes quickly. - Pre-calculating heights: If item heights can be determined without rendering (e.g., from data properties like
lineCount), you can pre-calculate and store them.
The most common robust approach involves a combination of estimation and measurement. You maintain a map of index -> { height, offset } and update it as items are rendered and measured. When scrollTop changes, you perform a binary search on this map to find the startIndex whose offset is just below scrollTop. It's significantly more complex but necessary for a truly general-purpose virtual scroller.
Libraries like react-window or react-virtualized implement these advanced strategies, and if you need a battle-tested solution, they are excellent choices. Understanding the core mechanism, however, helps you debug and optimize them, or even build a custom solution for niche needs.
Trade-offs and Gotchas
While virtual scrolling is powerful, it's not a silver bullet. Here are a few things to keep in mind:
- Complex
scrollTopmanagement: If you have external controls that changescrollTop(e.g., a

Internationalization in Next.js: Beyond the Basic `i18n.js` File
Setting up i18n in Next.js often starts with a simple JSON file, but real-world applications quickly outgrow that. `next-intl` offers a robust solution that integrates deeply with Next.js features, including Server Components, to manage translations more effectively and avoid common pitfalls.

Beyond the Canvas: When Your Frontend Becomes an AI Art Studio
Stable Diffusion, Midjourney, DALL-E 3 — these names are everywhere. But for frontend developers, the interaction often stops at calling an API or embedding a widget. InvokeAI, however, presents a different paradigm: bringing the full power of an AI art engine directly into a sophisticated web appli

Beyond the Marketing: When a No-Code Editor Becomes Your Component Playground
I've been wary of 'no-code' tools. They often promise the moon but deliver a walled garden, abstracting away too much and leaving you stranded when you need real customizability. GrapesJS, however, isn't just another drag-and-drop editor; it's a web builder framework. This distinction fundamentally


















