Virtual Scrolling Implementation
The application implements an efficient virtual scrolling system to handle large datasets while maintaining smooth performance. This document details the technical implementation and benefits of our virtual scrolling solution.
Design Principles
-
Minimize API Calls: Following Rule #1, the implementation is designed to minimize unnecessary API calls by:
- Using a sliding window approach that only loads data for visible rows plus buffer
- Maintaining a cache of loaded pages
- Cancelling pending requests when scrolling
- Cleaning up old pages to prevent memory bloat
-
Memory Efficiency: The system maintains only the necessary data in memory:
- Keeps a fixed window of rendered rows
- Cleans up pages outside the window range
- Uses efficient data structures (Map, Set) for tracking
-
Smooth Performance: Ensures smooth scrolling experience through:
- Request cancellation for outdated data
- DOM batch updates with DocumentFragment
- RAF-based rendering
- Efficient cache management
Overview
Virtual scrolling (also known as windowing) is a technique that significantly improves performance when rendering large lists by only rendering the items that are currently visible in the viewport, plus a small buffer.
Technical Implementation
Core Components
class VirtualScroll {
constructor(tableBody, options = {}) {
// Configuration
this.rowHeight = options.rowHeight || 48;
this.pageSize = 100; // Match API limit
this.windowSize = 200; // Number of rows to keep rendered
this.buffer = 50; // Buffer rows above/below visible area
// State
this.totalCount = 0;
this.loadedPages = new Map(); // Track loaded pages
this.scrollState = {
targetRow: 0,
pendingLoads: new Set(),
};
}
}
Key components include:
- Height Placeholder: Maintains correct scrollbar dimensions
- Content Container: Holds rendered items
- Buffer: Additional rows rendered above/below visible area
- Window Size: Number of rows to keep rendered
- Page Cache: Map of loaded data pages
- Pending Loads: Set of active request controllers
DOM Structure
<div class="virtual-scroll-container">
<div class="overflow-auto custom-scrollbar">
<!-- Total height placeholder -->
<div class="virtual-scroll-spacer"></div>
<!-- Rendered content -->
<div class="virtual-scroll-content"></div>
</div>
</div>
Performance Optimizations
1. Request Management
async loadPages(startPage, endPage) {
// Cancel any pending loads
this.scrollState.pendingLoads.forEach(controller => controller.abort());
this.scrollState.pendingLoads.clear();
// Load required pages
const pagePromises = [];
for (let page = startPage; page <= endPage; page++) {
if (!this.loadedPages.has(page)) {
pagePromises.push(this.loadPage(page));
}
}
// Load pages in parallel
if (pagePromises.length > 0) {
await Promise.all(pagePromises);
}
// Clean up pages outside window range
const keepRange = 1; // Number of pages to keep on each side
for (const page of this.loadedPages.keys()) {
if (page < startPage - keepRange || page > endPage + keepRange) {
this.loadedPages.delete(page);
}
}
}
Benefits:
- Cancels outdated requests
- Loads multiple pages in parallel
- Maintains clean page cache
- Prevents unnecessary API calls
2. Cache Integration
async loadPage(pageNumber) {
const controller = new AbortController();
try {
const offset = pageNumber * this.pageSize;
this.scrollState.pendingLoads.add(controller);
// Get data through cache
const result = await cacheManager.getOrFetch(
this.getBaseKey(),
() => this.options.onLoadMore(offset, this.pageSize, controller.signal),
offset,
this.pageSize
);
if (result) {
this.loadedPages.set(pageNumber, result);
}
return result;
} finally {
this.scrollState.pendingLoads.delete(controller);
}
}
Features:
- Integrates with cache manager
- Handles request cancellation
- Proper cleanup in finally block
- Error resilient
3. Efficient Rendering
render(windowStart, windowEnd) {
// Get data for window
const windowData = this.getDataWindow(windowStart, windowEnd);
// Create fragment for batch update
const fragment = document.createDocumentFragment();
const seenIds = new Set();
// Render rows
windowData.forEach((edge) => {
if (!edge?.node || seenIds.has(edge.node.id)) return;
seenIds.add(edge.node.id);
// Add cells to fragment
});
// Update DOM in one batch
requestAnimationFrame(() => {
this.contentContainer.innerHTML = "";
this.contentContainer.appendChild(fragment);
});
}
Optimizations:
- Uses DocumentFragment for batch updates
- Deduplicates rows with Set
- RAF for smooth rendering
- Minimal DOM operations
Memory Management
The virtual scrolling implementation includes several memory optimization strategies:
-
Page Management:
- Keeps limited pages in memory
- Cleans up pages outside window range
- Maintains small buffer for smooth scrolling
-
Request Cleanup:
destroy() {
// Cancel pending operations
if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
if (this.scrollRAF) cancelAnimationFrame(this.scrollRAF);
if (this.renderRAF) cancelAnimationFrame(this.renderRAF);
// Cancel pending loads
this.scrollState.pendingLoads.forEach(controller => controller.abort());
this.scrollState.pendingLoads.clear();
// Clean up DOM
this.contentContainer.innerHTML = "";
}
Usage Example
const virtualScroll = new VirtualScroll(tableBody, {
rowHeight: 48,
scrollContainer: container,
onLoadMore: async (offset, limit, signal) => {
// Fetch data with abort signal
const result = await dataLoader.loadTrackerData(offset, limit, signal);
return result;
},
renderRow: (edge) => {
// Return row HTML
return `<div>${edge.node.id}</div>`;
},
});
Performance Metrics
The virtual scrolling implementation achieves:
- Constant memory usage regardless of dataset size
- Smooth scrolling at 60fps
- < 16ms per frame for rendering
- Minimal API calls
- Efficient cache utilization
- Clean request management
Browser Support
The implementation is compatible with modern browsers and includes:
- AbortController for request cancellation
- RequestAnimationFrame for smooth animations
- Passive event listeners for scroll performance
- DocumentFragment for efficient DOM updates
- Map and Set for efficient data structures