Million Rows Performance
Render one million rows with virtual scrolling and cell pooling.
This example demonstrates ZenGrid’s ability to handle massive datasets with virtual scrolling and cell pooling. Render one million rows with smooth scrolling and constant DOM element count.
Basic Million Row Grid
import { createGrid } from '@zengrid/core';import type { Column, GridOptions } from '@zengrid/core';
const columns: Column[] = [ { id: 'id', header: 'ID', width: 80 }, { id: 'name', header: 'Name', width: 200 }, { id: 'email', header: 'Email', width: 250 }, { id: 'department', header: 'Department', width: 150 }, { id: 'salary', header: 'Salary', width: 120 }, { id: 'startDate', header: 'Start Date', width: 150 },];
const grid = createGrid(container, { columns, rowCount: 1_000_000, // One million rows rowHeight: 40, headerHeight: 44,
// Data generation function data: (rowIndex: number, colId: string) => { switch (colId) { case 'id': return rowIndex + 1; case 'name': return `Employee ${rowIndex + 1}`; case 'email': return `employee${rowIndex + 1}@company.com`; case 'department': return ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance'][rowIndex % 5]; case 'salary': return `$${(50000 + (rowIndex * 100) % 100000).toLocaleString()}`; case 'startDate': const date = new Date(2020, 0, 1); date.setDate(date.getDate() + (rowIndex % 1000)); return date.toISOString().split('T')[0]; default: return ''; } },});
console.log('Grid created with 1,000,000 rows');console.log('DOM element count:', document.querySelectorAll('.zengrid-cell').length);Only visible cells are rendered in the DOM. With a viewport height of 600px and 40px row height, only ~15 rows are rendered at any time, regardless of the total row count.
Cell Pooling
Enable cell pooling to reuse DOM elements during scrolling:
const grid = createGrid(container, { columns, rowCount: 1_000_000, rowHeight: 40,
// Enable cell pooling enableCellPooling: true, cellPoolSize: 100, // Pool size
data: (rowIndex, colId) => { // Data generation return `Cell ${rowIndex}-${colId}`; },});Cell pooling reduces memory allocation and garbage collection during scrolling. It’s enabled by default for grids with more than 10,000 rows.
Overscan for Smooth Scrolling
Add overscan rows to improve scroll smoothness:
const grid = createGrid(container, { columns, rowCount: 1_000_000, rowHeight: 40,
// Render extra rows above and below viewport overscan: 5, // Render 5 extra rows on each side
data: (rowIndex, colId) => { return `Cell ${rowIndex}-${colId}`; },});Overscan renders additional rows outside the visible viewport. This reduces blank areas during fast scrolling but increases the number of rendered cells.
Performance Monitoring
Monitor grid performance with built-in statistics:
const grid = createGrid(container, options);
// Get performance statsconst stats = grid.getStats();
console.log('Performance Stats:', { totalRows: stats.totalRows, visibleRows: stats.visibleRows, renderedCells: stats.renderedCells, domElements: stats.domElements, scrollTop: stats.scrollTop, renderTime: stats.lastRenderTime, fps: stats.averageFps,});
// Monitor stats during scrollingsetInterval(() => { const stats = grid.getStats(); console.log(`FPS: ${stats.averageFps}, Render: ${stats.lastRenderTime}ms`);}, 1000);Navigation with ScrollToCell
Navigate to specific cells programmatically:
const grid = createGrid(container, options);
// Scroll to specific rowgrid.scrollToCell({ rowIndex: 500_000 });
// Scroll to specific cell with alignmentgrid.scrollToCell({ rowIndex: 750_000, colId: 'email', align: 'center', // 'start', 'center', 'end'});
// Smooth scroll to cellgrid.scrollToCell({ rowIndex: 999_999, // Last row behavior: 'smooth',});
// Add navigation buttonsfunction addNavigationButtons() { const nav = document.createElement('div'); nav.innerHTML = ` <button id="scroll-start">Start</button> <button id="scroll-middle">Middle</button> <button id="scroll-end">End</button> <input type="number" id="row-input" placeholder="Row number" /> <button id="scroll-to-row">Go</button> `; document.body.prepend(nav);
document.getElementById('scroll-start')?.addEventListener('click', () => { grid.scrollToCell({ rowIndex: 0 }); });
document.getElementById('scroll-middle')?.addEventListener('click', () => { grid.scrollToCell({ rowIndex: 500_000 }); });
document.getElementById('scroll-end')?.addEventListener('click', () => { grid.scrollToCell({ rowIndex: 999_999 }); });
document.getElementById('scroll-to-row')?.addEventListener('click', () => { const input = document.getElementById('row-input') as HTMLInputElement; const rowIndex = parseInt(input.value, 10); if (!isNaN(rowIndex) && rowIndex >= 0 && rowIndex < 1_000_000) { grid.scrollToCell({ rowIndex }); } });}
addNavigationButtons();Use scrollToCell() to implement features like “jump to row”, search results navigation, or bookmarks.
Complete Million Row Example
import { createGrid } from '@zengrid/core';import type { Column, GridOptions } from '@zengrid/core';
// Generate realistic mock datafunction generateData(rowIndex: number, colId: string): string | number { const seed = rowIndex; const random = (min: number, max: number) => { return Math.floor((Math.sin(seed * 9999 + colId.length) + 1) * 0.5 * (max - min) + min); };
switch (colId) { case 'id': return rowIndex + 1; case 'name': const firstNames = ['John', 'Jane', 'Mike', 'Sarah', 'David', 'Emily']; const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones']; return `${firstNames[random(0, firstNames.length)]} ${lastNames[random(0, lastNames.length)]}`; case 'email': return `user${rowIndex + 1}@company.com`; case 'department': return ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance'][random(0, 5)]; case 'salary': return `$${(50000 + random(0, 100000)).toLocaleString()}`; case 'startDate': const date = new Date(2015, random(0, 12), random(1, 28)); return date.toISOString().split('T')[0]; case 'performance': return `${random(60, 100)}%`; default: return ''; }}
// Column definitionsconst columns: Column[] = [ { id: 'id', header: 'ID', width: 80, frozen: true }, { id: 'name', header: 'Name', width: 200 }, { id: 'email', header: 'Email', width: 250 }, { id: 'department', header: 'Department', width: 150 }, { id: 'salary', header: 'Salary', width: 120 }, { id: 'startDate', header: 'Start Date', width: 150 }, { id: 'performance', header: 'Performance', width: 120 },];
// Create gridconst grid = createGrid(container, { columns, rowCount: 1_000_000, rowHeight: 40, headerHeight: 44, enableCellPooling: true, overscan: 5, data: generateData,});
// Performance dashboardconst dashboard = document.createElement('div');dashboard.style.cssText = 'padding: 10px; background: #f5f5f5; margin-bottom: 10px;';document.body.prepend(dashboard);
function updateDashboard() { const stats = grid.getStats(); dashboard.innerHTML = ` <strong>Performance Dashboard</strong> | Total Rows: ${stats.totalRows.toLocaleString()} | Visible: ${stats.visibleRows} | DOM Cells: ${stats.renderedCells} | FPS: ${stats.averageFps} | Render: ${stats.lastRenderTime}ms | Scroll: ${Math.round(stats.scrollTop)}px `;}
// Update dashboard every secondsetInterval(updateDashboard, 1000);updateDashboard();
// Navigation controlsconst nav = document.createElement('div');nav.style.cssText = 'padding: 10px; margin-bottom: 10px;';nav.innerHTML = ` <button id="start">⏮ Start</button> <button id="prev-page">◀ Prev Page</button> <button id="middle">⏺ Middle</button> <button id="next-page">Next Page ▶</button> <button id="end">End ⏭</button> <input type="number" id="row-input" placeholder="Row" style="width: 100px; margin-left: 10px;" /> <button id="go">Go</button>`;document.body.prepend(nav);
// Navigation handlersdocument.getElementById('start')?.addEventListener('click', () => { grid.scrollToCell({ rowIndex: 0, behavior: 'smooth' });});
document.getElementById('middle')?.addEventListener('click', () => { grid.scrollToCell({ rowIndex: 500_000, behavior: 'smooth' });});
document.getElementById('end')?.addEventListener('click', () => { grid.scrollToCell({ rowIndex: 999_999, behavior: 'smooth' });});
document.getElementById('prev-page')?.addEventListener('click', () => { const stats = grid.getStats(); const currentRow = Math.floor(stats.scrollTop / 40); const targetRow = Math.max(0, currentRow - 20); grid.scrollToCell({ rowIndex: targetRow, behavior: 'smooth' });});
document.getElementById('next-page')?.addEventListener('click', () => { const stats = grid.getStats(); const currentRow = Math.floor(stats.scrollTop / 40); const targetRow = Math.min(999_999, currentRow + 20); grid.scrollToCell({ rowIndex: targetRow, behavior: 'smooth' });});
document.getElementById('go')?.addEventListener('click', () => { const input = document.getElementById('row-input') as HTMLInputElement; const rowIndex = parseInt(input.value, 10) - 1; // Convert to 0-based if (!isNaN(rowIndex) && rowIndex >= 0 && rowIndex < 1_000_000) { grid.scrollToCell({ rowIndex, behavior: 'smooth' }); }});
// Log scroll eventslet lastLog = Date.now();grid.on('scroll', (event) => { const now = Date.now(); if (now - lastLog > 1000) { console.log('Scrolled to row ~', Math.floor(event.scrollTop / 40)); lastLog = now; }});
console.log('Million row grid initialized');The grid maintains constant performance regardless of the total row count. Scrolling from row 1 to row 1,000,000 is just as smooth as scrolling within the first 100 rows.
Performance Tips
Data Generation
- Keep the
datacallback function fast - Avoid expensive computations or API calls inside
data - Pre-compute or cache complex values
// Bad: Expensive operation in data callbackdata: (rowIndex, colId) => { return expensiveComputation(rowIndex, colId); // Called frequently!}
// Good: Cache computed valuesconst cache = new Map();data: (rowIndex, colId) => { const key = `${rowIndex}-${colId}`; if (!cache.has(key)) { cache.set(key, expensiveComputation(rowIndex, colId)); } return cache.get(key);}Cell Rendering
- Use simple HTML in cell renderers
- Avoid nested elements when possible
- Minimize CSS complexity on cells
Memory Management
- Don’t store references to all rows in memory
- Use generators or lazy loading for data access
- Enable cell pooling for very large grids
Virtual scrolling renders only visible cells. Don’t try to access DOM elements for non-visible rows as they don’t exist in the DOM.
Benchmarks
Typical performance on modern hardware:
- 1 million rows: 60 FPS smooth scrolling
- Render time: under 16ms per frame
- DOM elements: Constant (~150 cells regardless of row count)
- Memory usage: ~50MB for 1M rows with 7 columns
- Scroll to any row: under 100ms
Next Steps
- Add sorting to large datasets
- Add filtering to narrow down results
- Learn about data virtualization strategies
- Explore performance optimization techniques