Start with the work, not the row count
A requirement for 100,000 rows and 200 columns describes a potential dataset of twenty million cells. It does not describe how much work the browser must perform at once. A user may see thirty rows and twelve columns, edit a single number, filter the complete dataset, or export every record. Each action exercises a different part of the system. A useful evaluation begins by separating these actions and defining which of them must remain responsive together.
This is an evaluation design, not a measured result for that exact matrix. The supplied CompareGrids snapshot contains several row counts and a separate wide-column scenario, but it does not establish performance for your particular combination of 100,000 rows, 200 columns and production renderers. Treat its results as evidence for choosing candidates to investigate. Reproduce the final workload before making a capacity claim.
Define the data contract
Write down the physical and logical shape of the data. Physical shape includes field types, typical string lengths, null frequency and the distribution of repeated values. Logical shape includes relationships, calculated columns, grouping keys and the uniqueness of identifiers. Two datasets with the same cell count can have very different transfer sizes and processing costs. Repeated status codes and long unique descriptions should not share one synthetic test without explanation.
Generate a deterministic fixture with a recorded seed. Include ordinary records, the largest expected records and awkward values such as empty strings, null dates and non-ASCII text. Keep the fixture free of real customer information. A generator that produces only short integers may produce a clean benchmark while missing the actual bottleneck: formatting, measurement or allocation around strings and rich content.
Distinguish source fields from displayed columns. A view with 200 columns might derive many values from a much smaller source record. Conversely, the application might retain fields that never appear on screen. Record both counts. Otherwise, a comparison can accidentally reward one implementation for doing less work or punish another for retaining data needed by a later workflow.
Separate rendering from processing
Virtualization limits the elements rendered near the viewport. It does not automatically limit the records held in memory or the rows processed by a filter. AG Grid's documentation describes row and column DOM virtualization; its implementation details are evidence about that product, not a promise that every grid uses the same strategy. [1] Test horizontal and vertical movement independently, then test diagonal movement with pinned columns enabled.
For initial display, measure distinct intervals: request to first byte, download, parsing or decoding, data transformation, grid initialization, and first usable view. Keep an end-to-end measurement alongside the components. The sum of isolated measurements may differ from elapsed time because tasks overlap. The user cares when they can begin a reliable action, not merely when the grid object has been constructed.
For processing, include a simple numeric sort, a multi-column sort with ties, a selective filter, a broad filter, grouping and at least one calculated value. Validate output against an independent reference implementation. A fast answer is not useful if null handling or collation changes which records appear. State whether processing runs in the browser, a worker or a service.
Build a realistic cell budget
Create three renderer profiles. The first uses plain text with fixed heights. The second adds the formatters and icons typical of the application. The third represents the demanding supported view, including controls, validation messages or wrapped descriptions. Use the same visible information in every candidate. This reveals how much performance comes from the core grid and how much comes from application code.
Count visible cells and mounted controls, not just total rows. With thirty visible rows and twelve visible columns, the nominal viewport contains 360 cells before overscan, pinned regions and headers. This arithmetic is an illustration, not a DOM measurement. Inspect the actual page to find duplicated panes, extra wrappers and retained editors. A component that looks lightweight can still mount substantial hidden work.
Variable row heights deserve their own scenario. Open a long description, resize a column, change the font size and scroll back to a previously visited position. Watch for repeated measurements and jumps. Do not enable expensive height behavior for all rows unless the product actually needs it. Equally, do not disable it during evaluation if users depend on it every day.
Measure a session, not one screenshot
Use a repeatable sequence: open the view, scroll to the middle, sort, edit a record, change a filter, navigate away and return. Repeat the sequence with a warm cache and with a fresh page. Include rapid successive actions. A response from an earlier filter must never replace the results of a later filter simply because the earlier request finished last.
Record interaction latency and correctness at each step. For editing, distinguish opening the editor, accepting input, committing locally and receiving server confirmation. For sorting, end the measurement when visible rows and sort indicators agree. For export, verify the file contents rather than stopping at a download notification. These boundaries turn vague claims of responsiveness into reproducible observations.
Set acceptance thresholds before testing. A support dashboard and an occasional analytical report may tolerate different delays. Ask representative users to complete the actual task and establish where waiting becomes disruptive. Avoid treating an arbitrary universal millisecond target as a business requirement. Record the chosen threshold, the affected workflow and who approved it.
Keep memory evidence separate
The supplied snapshot does not include memory measurements, so it cannot support a memory ranking. Add memory observation to your own proof of concept. Compare the baseline page, the loaded dataset, the grid after interaction and the page after teardown. Capture whether repeated visits lead to a sustained increase, while recognizing that garbage collection and browser accounting introduce noise.
Do not estimate JavaScript memory by multiplying wire size by a fixed factor. Strings, objects, indexes, duplicated records and caches vary by implementation. A smaller download can still lead to more allocations after decoding. Keep peak memory, retained memory and transfer bytes as separate outputs. If the available measurement excludes some browser or graphics allocations, document that limitation.
Test the lowest supported device class. The benchmark machine has 64 GB of RAM and an Intel i9-13900HX processor; that is a documented environment, not a model of every employee laptop. A design that approaches a limit on this machine needs particular scrutiny. Device testing also catches thermal throttling and contention from the rest of a real application.
Use the snapshot as a map of risk
| Scenario | Numerical | Lock | Crash | Fail |
|---|---|---|---|---|
| Plain creation | 56 | 3 | 2 | 1 |
| Wide columns | 46 | 7 | 7 | 2 |
Across the 62 entries in the supplied data, the one-million-row plain-creation field contains 56 numerical results, three lock or excessive-duration codes, two crash codes and one failure code. The wide-column field at that row count contains 46 numerical results, seven crash codes, seven lock codes and two failure codes. These counts describe the supplied implementations and status coding, not permanent product capabilities. Inspect supplied results
They also show why successful plain creation is an incomplete selection criterion. Expanding the workload changes the completion profile. Do not average failure codes into timing values or discard them silently. Keep a completion table next to any timing chart. A product that completes a demanding operation should remain distinguishable from one whose timing is unavailable.
The source selects relatively fast observations rather than reporting medians. Its initialization measurements also exclude script and dataset loading. Those choices are explained in the methodology and should travel with any quoted number. Your application may have a different dominant cost, especially when a remote dataset must be downloaded before initialization begins.
Keep a results sheet that explains the decision
For each operation, retain the expected output, observed completion status, timing boundary and device configuration. Add the exact renderer profile and whether the data was already local. This prevents a later reader from comparing a warm plain-text sort with a cold remote-data interaction merely because both rows are labelled sorting.
Include one deliberately demanding but supported case and one ordinary daily case. The demanding case identifies capacity risk, while the ordinary case represents most user time. A decision based only on extremes can overengineer the product; a decision based only on the easiest case can conceal a launch blocker. State which case determines acceptance and which is informational.
A proof of concept you can hand over
Deliver a small runnable scenario with pinned dependency versions, a fixed dataset seed, explicit grid configuration and an action script. Include screenshots of the expected state and a machine-readable record of results. Record failures even when they are inconvenient. Keep a short explanation of every candidate-specific optimization so a future maintainer can repeat the work.
Give each candidate the same opportunity for documented tuning. If one requires an unusual renderer or a custom cache to meet the target, include its implementation and maintenance cost in the decision. Fair comparison means equivalent outcomes under disclosed configurations, not identical code regardless of architecture. Record which optimizations are part of ordinary supported use.
The final recommendation should name the workload it supports: dataset shape, visible complexity, operations, device class and acceptable delays. It should also identify the next boundary that has not been tested. That is more useful than saying a grid handles twenty million cells. It gives engineering and procurement a claim they can verify when the application, browser or dataset changes.