Rendering 100K Rows with Virtual Scrolling
Modern web applications are routinely tasked with displaying massive datasets, from real-time log viewers to complex financial transaction histories. However, attempting to render a standard HTML list or table with tens of thousands of rows will quickly crash the browser.
Every DOM node memory footprint grows, and the browser's rendering engine slows down. Operations like style calculation, reflow, and painting become prohibitively expensive, leading to catastrophic lag, frame drops, and unresponsive interfaces. To solve this, developers must implement virtual scrolling—a performance pattern that renders only the subset of items currently visible within the viewport, while simulating a fully rendered container scrollbar.
The Math Behind Virtualization
Virtual scrolling relies on a simple geometric calculation. Instead of rendering all 100,000 items, we determine the height of a single row and calculate how many rows can fit inside the container's visible area (the viewport).
The total height of the scroll container is set to the total number of items multiplied by the row height. This tricks the browser into rendering a native scrollbar that accurately reflects the full dataset size. The key parameters needed are:
scrollTop: The current vertical scroll position of the viewport.viewportHeight: The visible height of the container.rowHeight: The height of a single item in pixels (assuming uniform height for simplicity).totalItems: The total number of items in the dataset (e.g., 100,000).overscan: The number of extra rows to render above and below the visible region to prevent empty space from appearing during fast scrolls.
Using these variables, we calculate the range of indexes that should be rendered:
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const endIndex = Math.min(totalItems - 1, Math.floor((scrollTop + viewportHeight) / rowHeight) + overscan);
The visible items are then positioned absolutely within the scroll container, with their top offset calculated as index * rowHeight. This ensures they line up correctly as the user scrolls, while the actual DOM contains only a constant, tiny fraction of the total rows (e.g., 30 items instead of 100,000).
Vanilla JavaScript Implementation of a Virtual Scroller
To understand the performance characteristics, let us build a dependency-free virtual scroller in TypeScript. This implementation uses a scroll container, a spacer element to simulate the total height, and a wrapper to position the rendered rows:
class VirtualScroll {
private viewport: HTMLElement;
private spacer: HTMLElement;
private content: HTMLElement;
private items: string[];
private rowHeight: number;
private overscan: number;
constructor(viewport: HTMLElement, items: string[], rowHeight = 35, overscan = 5) {
this.viewport = viewport;
this.items = items;
this.rowHeight = rowHeight;
this.overscan = overscan;
// Create the spacer to simulate total height
this.spacer = document.createElement("div");
this.spacer.style.height = `${this.items.length * this.rowHeight}px`;
this.spacer.style.width = "100%";
this.spacer.style.position = "absolute";
this.spacer.style.top = "0";
this.spacer.style.left = "0";
this.spacer.style.zIndex = "-1";
// Create the content container for visible rows
this.content = document.createElement("div");
this.content.style.position = "absolute";
this.content.style.top = "0";
this.content.style.left = "0";
this.content.style.width = "100%";
this.viewport.style.position = "relative";
this.viewport.style.overflowY = "auto";
this.viewport.appendChild(this.spacer);
this.viewport.appendChild(this.content);
// Bind scroll handler
this.viewport.addEventListener("scroll", () => this.render());
this.render();
}
public render() {
const scrollTop = this.viewport.scrollTop;
const viewportHeight = this.viewport.clientHeight;
const totalItems = this.items.length;
const startIndex = Math.max(0, Math.floor(scrollTop / this.rowHeight) - this.overscan);
const endIndex = Math.min(totalItems - 1, Math.floor((scrollTop + viewportHeight) / this.rowHeight) + this.overscan);
// Clear previous items
this.content.innerHTML = "";
// Position container dynamically to offset scroll position
const offsetY = startIndex * this.rowHeight;
this.content.style.transform = `translate3d(0, ${offsetY}px, 0)`;
// Render visible slice
const fragment = document.createDocumentFragment();
for (let i = startIndex; i <= endIndex; i++) {
const row = document.createElement("div");
row.style.height = `${this.rowHeight}px`;
row.style.boxSizing = "border-box";
row.className = "virtual-row";
row.textContent = `Row ${i + 1}: ${this.items[i]}`;
fragment.appendChild(row);
}
this.content.appendChild(fragment);
}
}
In this implementation, we utilize translate3d instead of modifying the top property of each row individually. This shifts the entire visible block using the GPU's compositing layer, reducing layout calculation times and ensuring 60fps scrolling performance, a technique comparable to double-buffered canvas rendering to prevent flicker.
Handling Dynamic Row Heights
The implementation gets significantly more complex when dynamic row heights (such as text-wrapping comments) are introduced. If we cannot predict the height of a row in advance, we must measure rows dynamically and maintain a cache of coordinates in browser storage, utilizing IndexedDB wrapper patterns for persistence if necessary.
When a row is rendered for the first time, its actual height is measured using getBoundingClientRect(), and the scroll height cache is updated. An accumulation index is then built, and binary search is used to locate the startIndex for a given scrollTop, maintaining 60fps performance. To keep the main thread free during intensive binary searches, calculations can be offloaded to Web Workers.
Enterprise Data Visualization at the Edge with Bramsley
Managing real-time telemetry dashboards requires aligning client-side rendering with edge caching models. Bramsley Digital Studio optimizes enterprise data pipelines to guarantee 60fps interactions:
- Edge-Optimized Streaming: Lazy-loading data streams directly from edge workers to minimize DOM injection overhead.
- Virtual Grid Engineering: Custom virtualized lists utilizing 3D transforms for lag-free performance on millions of logs.
- Web Vital Guardianship: Tuning layouts to reduce Core Web Vitals like Cumulative Layout Shift (CLS) and Interaction to Next Paint (INP) to sub-millisecond levels.
Let Bramsley tune your data-heavy dashboards. Partner with us to build a lag-free, responsive enterprise application.