Client virtualization for browser-owned working sets
Load the records once when payload size, memory, client sorting, and client filtering remain acceptable. Virtualization bounds DOM work but the browser still stores and processes the dataset.
Infinite loading for sequential discovery
Infinite loading suits feeds or search results where users append pages and do not need a complete global sort, group, or pivot over unloaded records. Define cursors, duplicate prevention, retry, and end-of-data behavior.
Server row model for remote operations
Choose Enterprise when the server must own filtering, sorting, grouping, pivoting, or paging. Define request fields, stable ordering, total-row semantics, block cache, cancellation, retries, edits, invalidation, and authorization before UI work.
Test network and query failures
Simulate latency, timeouts, cancellation, stale responses, partial blocks, rate limits, and permission errors. Keep the current view stable while a new query loads when that helps users maintain context. Ensure rapid filter changes do not allow older results to replace newer ones. Log request identifiers for diagnosis without storing sensitive query data.
Choose where global truth lives
A large data grid decision starts with ownership. If the browser owns the full working set, Core virtualization may be enough. If users discover records sequentially, infinite loading may be enough. If the server owns global filtering, sorting, grouping, paging, or permissions, Enterprise server row model behavior becomes relevant. This ownership framing is more useful than discussing row count alone.
Test stale and out-of-order responses
Rapid filters, slow networks, retries, and cancellation can produce stale responses. The grid should not allow an older request to replace newer results. Track request IDs and keep visible state coherent while loading. This is one of the most important practical differences between a demo with many rows and a production large-data React grid.
Match the strategy to search behavior
If users search or filter globally, the server probably needs to own the operation. Infinite loading is not enough when unloaded rows must participate in the result. Many large-grid mistakes come from optimizing scroll while ignoring search correctness.
Define row identity across pages
Stable row IDs must survive pagination, cursor loading, sorting, filtering, refresh, and edits. Duplicate or shifting IDs break selection, dirty state, focus, and audit trails. This is a practical requirement for any large data grid in React and should be repeated because it is more important than raw row count.
Select a data strategy with evidence
Choose client virtualization when the browser can own the full working set and local operations remain within budget. Choose infinite loading when users discover sequential pages and global operations across unloaded rows are not required. Choose the Enterprise server row model when the server owns filtering, sorting, grouping, pivoting, paging, permissions, or global totals. Record the required semantics before implementation.
Define bulk action and export scope
Specify whether selection means visible rows, loaded rows, explicit IDs, or every record matching a server-side filter. Display that scope to users. Global export and bulk action require a backend query, authorization, size limits, progress, cancellation, and audit records because the browser cannot safely act on records it never loaded.
Measure backend and browser budgets separately
Track query duration, payload size, serialization, transfer, client processing, render time, and memory as separate measurements. This prevents frontend virtualization from being credited for a backend improvement or blamed for a slow query. Set budgets from the actual user workflow and target network conditions.
Large data grid React implementation
import { Grid } from "@ace-grid/enterprise";
export function ServerBackedGrid({ rows, columns }) {
return (
<Grid
data={{ rows, columns }}
layout={{ width: 1200, height: 620 }}
columns={{ columnWidths: {} }}
serverRowModel={{
enabled: true,
blockSize: 200,
getRows: ({ startRow, endRow, sortModel, filterModel }) =>
fetchAccountRows({ startRow, endRow, sortModel, filterModel }),
}}
charts={{ enabled: true }}
pivot={{ enabled: true }}
masterDetail={{ enabled: true }}
/>
);
}