Two different limits
Virtualization controls how much interface is rendered. Lazy loading controls when data or code is acquired. They often appear together because a large dataset creates both problems, but neither implies the other. A grid can download every record and render only the viewport. It can also fetch one page at a time and render every row in that page. Understanding the distinction prevents misleading capacity claims and incorrect application behavior.
Consider a million-record database with forty visible rows. The browser might hold forty records, four hundred cached records or all one million. The screen can look identical in each case. A screenshot therefore tells you very little about the data architecture. Ask where the complete dataset lives, how the visible range is chosen and which component executes operations over the full result set.
What virtualization saves
In a virtualized view, the rendered region follows the viewport. A buffer outside that region can prepare nearby content before it appears. The exact size and behavior are implementation choices. AG Grid documents DOM virtualization in both dimensions, including the insertion and removal of elements during scrolling. [1] Use that as a concrete example, then verify the behavior of the version and configuration you intend to use.
This can reduce browser layout and rendering work when a dataset is much larger than the screen. It does not erase the cost of parsing the dataset, retaining it or building indexes. An application can show only a small number of elements and still spend significant time sorting every record. Diagnose the stage that is slow before changing a viewport setting.
Horizontal virtualization matters for wide tables. A view with a modest number of rows but hundreds of columns can mount many cells if only the row direction is virtualized. Pinned columns, merged cells and dynamic heights complicate the visible region. Include these features in a proof of concept rather than assuming that a successful plain table demonstrates the final workload.
What lazy loading saves
Data loading on demand can reduce the initial transfer and the amount retained locally. The application requests a page, a block or the children of an expanded group. This shifts responsibility to the service: it must answer requests consistently, apply access control and explain whether more results exist. Loading fewer records is an architectural choice with observable semantics, not merely a visual optimization.
A cache makes the distinction less obvious. Previously visited blocks may remain available, while unseen blocks require a request. Specify the cache budget and eviction behavior. Moving back through a table should not unexpectedly lose an edit or change a selected record. If selection spans unloaded records, an array of currently loaded row objects is not a sufficient representation of that selection.
Code splitting is another use of lazy loading. Deferring an export module can reduce the initial JavaScript payload without changing how row data is fetched. Measure it separately. The first export may incur a module download that subsequent exports avoid. Calling both mechanisms lazy loading is common, but an evaluation should name the resource being deferred.
Sorting exposes the architecture
Suppose a user sorts by revenue while only the first block is present. Sorting that block locally does not produce a globally sorted result. The service must apply the sort to the eligible dataset, or the client must already have the full dataset required for the operation. The same issue applies to filtering, grouping, aggregates and search.
Define a query contract that includes sort fields, directions, filters and a stable tie-breaker. Validate supported fields on the service. A visible column label should not become an unchecked query expression. Keep the query state distinct from presentation state such as column width. This makes exports and saved views easier to reproduce without coupling the backend to a particular grid's internal objects.
Mixed execution can be valid, but it must be deliberate. A server may apply the authoritative filter while the browser performs a temporary presentation sort within a small completed result. Tell users what the view represents. Avoid a control labelled search all records if it only searches loaded rows. Accurate labels are part of data correctness.
Pagination and scrolling are interfaces
A continuous scrollbar does not prove that all data is local. Likewise, page buttons do not prove that operations are server-side. Pagination describes how users navigate; it does not uniquely identify where computation happens. Evaluate the data model and the navigation model separately, then decide which combination supports the task.
For predictable record counts and stable ordering, offset-based requests can be straightforward. Frequently changing datasets raise additional consistency questions. Inserts or deletes between requests can move records across boundaries. A cursor or snapshot-based contract may be more suitable, depending on the service. Test duplicate and missing records by changing the dataset while navigating, rather than relying only on a static fixture.
Unknown totals also affect the interface. An estimated count should be labelled as an estimate, and a scrollbar should not suggest precision the service cannot provide. Some workflows are better served by progressive results with a clear continuation action. The appropriate navigation model depends on whether users browse, locate a known record or perform an exhaustive review.
Latency becomes visible during movement
When scrolling requests data, the experience includes network delay, service processing and rendering. A blank area might mean that the service has not returned records, while a skeleton might deliberately represent pending work. Instrument these states separately. Increasing the rendering buffer cannot solve a slow query if the necessary data has not arrived.
Prefetch can improve ordinary movement, but it has costs. Fetching too far ahead consumes bandwidth and may collect records the user never views. Rapid changes of direction can waste requests. Define cancellation and stale-response handling, and verify that a slow previous request cannot overwrite the current query. Treat these as correctness requirements before tuning perceived speed.
Offline behavior reveals another boundary. A fully loaded local dataset may remain usable without a connection. A partially cached remote view can only promise the operations its cached state supports. Show a meaningful error or restricted state when a new block cannot be fetched. An empty grid should not silently mean either no results or no network.
Test the combination you will ship
TanStack's virtualization guide makes an instructive architectural distinction: its table functionality can be combined with a separate virtualizer. [2] A headless row model and a rendering window are distinct components. That flexibility lets teams choose their interface, but it also means the integration owns synchronization between visible indexes, row identities and state.
Build tests around identity rather than position. Select a row, sort the dataset and confirm that the same record remains selected. Start editing, scroll away and return. Expand a group, change a filter and inspect both loaded and unloaded branches. These scenarios reveal bugs that a fast continuous scroll through static numbers will not expose.
For a useful report, publish three numbers separately: records in the authoritative result, records cached by the client and cells currently rendered. Add where each operation executes and what happens when a request fails. That description is far more informative than a single claim of support for one million rows. It also gives a project manager the vocabulary to ask the next practical question: which user actions remain correct when only part of the data is present?