Build a large table without freezing the browser

A page fetches thousands of records quickly, yet the browser freezes while the table appears. Adding memo to rows helps little because the initial render still creates every row and formats every cell. Another version virtualizes the rows but still sorts the entire dataset on every keystroke.
A large table has several cost centers: fetching, data transformation, rendering, layout, and interaction. The smallest useful optimization targets the measured bottleneck. This article uses an illustrative administrative event table and explains how pagination and virtualization solve different problems.
Measure the complete interaction
Record initial load, typing in a filter, changing sort, selecting rows, and scrolling. Use browser performance tools and React profiling to separate main-thread work from network time. A fast API does not establish a fast page.
Inspect dataset size, visible row count, and cell complexity. Ten thousand simple rows differ from ten thousand rows containing menus, icons, tooltips, and expensive date formatting. Measure representative synthetic data rather than using a tiny fixture that hides the cost.
Check the build and device context. Development checks and profiling add overhead, while a powerful laptop may hide problems users encounter. Report the environment and actual observations without inventing benchmarks or promising a universal speed improvement.
Bound fetching before optimizing rendering
If users only need a page of recent events, fetch a bounded page from the server. Loading the entire history just to display the first screen wastes bandwidth and memory. Define ordering, cursor or offset semantics, and filter behavior before changing the client.
Server pagination must preserve the query contract. Sorting a single downloaded page locally is not the same as sorting the whole dataset. Make the UI clear about whether filters and sorting apply globally or only to loaded records.
Use stable ordering and identifiers. Concurrent inserts can affect offset pagination, while cursor pagination needs a well-defined boundary and tie-breaker. The guide on cursor pagination develops that storage-facing contract.
Remove repeated data preparation
Inspect sorting, filtering, grouping, and formatting. An inline operation that rebuilds the full dataset on every local menu change can dominate interaction cost. Separate inputs that affect the derivation from unrelated UI state.
Memoize an expensive pure derivation when its inputs remain unchanged and measurement supports it. Do not mutate the original data while sorting, because mutation can confuse identity-based updates and other consumers. Preserve stable row identifiers through transformations.
Precompute or reuse formatters where appropriate, while keeping locale and timezone in the dependency contract. A cached formatter using the wrong timezone is not an acceptable performance improvement. Data preparation needs the same correctness review as rendering.
Choose pagination when the journey permits it
Pagination keeps both fetched and rendered work bounded and is often the simplest design for administrative lists. It works well when users review a subset, navigate deliberately, and can search or filter on the server.
Define selection across pages. Does “select all” mean the visible page or every record matching the current filter? The server action may need a filter-based operation rather than a list of currently rendered row identifiers. This is a product contract, not a minor checkbox detail.
Preserve filters and sort when moving pages, and reset or validate the page boundary when the query changes. A stale cursor from a different filter should not silently produce confusing results. The page's loading and error states should identify the requested slice.
Use virtualization for long continuous views
Virtualization renders a window of rows while preserving a scrollable representation of the larger list. It can reduce DOM work when users need continuous scrolling. It does not automatically reduce full-dataset fetching or expensive preprocessing.
Choose a library or implementation compatible with the project's row heights, sticky headers, keyboard navigation, and table semantics. Variable-height rows require a measurement strategy. Expanding a row can change offsets and create scroll jumps if the implementation assumes fixed heights.
Test focus carefully. A focused row or button can be unmounted when it leaves the rendered window. Keyboard navigation and screen-reader interpretation need explicit verification. Do not declare the table accessible merely because the original non-virtualized markup used table elements.
Use AI to compare concrete alternatives
Give an assistant the measured cost, dataset contract, and user tasks. Ask it to compare bounded server pagination with virtualization and identify the interaction changes each requires. Require it to keep performance claims conditional until measured.
An illustrative request is: “Initial rendering dominates this event table. Users usually inspect recent records and occasionally select all matching results. Compare pagination and virtualization, including selection semantics and accessibility. Propose one bounded change.” This yields a decision tied to the product rather than a fashionable optimization.
Use the assistant to find expensive cell work and repeated transformations, then verify those claims in a trace. It may suggest useMemo for trivial code while missing a third-party menu mounted in every row. Evidence should determine priority.
Verify responsiveness and correctness together
Repeat the same interactions after the candidate change. Check initial load, filter edits, sort changes, scrolling, and selection. Compare measured work in the same environment and retain the trace if it supports a material claim.
Test empty data, loading additional pages, errors, and rapid filter changes. Late responses must not append rows to the wrong query. Verify that duplicate or missing records do not appear across pagination boundaries and that row keys remain stable.
Check accessible names, header relationships, keyboard behavior, and local horizontal scrolling where the table is wide. Native table semantics help communicate relationships, but custom interaction and virtualization can change what users experience.
Keep limits explicit. Pagination changes navigation, virtualization complicates focus and measurement, and memoization consumes memory while requiring stable inputs. The useful result is a table that performs less unnecessary work and still preserves the intended data and interaction contract.
Verify export and search behavior separately from the visible rows. A paginated interface may show only one page while users expect an export to include every record matching the filter. Keep the server-side filter contract reusable for that operation and make the scope explicit in the UI. Likewise, browser find may only search mounted virtualized rows, so users may need an application search that covers the full dataset. These differences should be deliberate product decisions rather than surprises introduced by an optimization.
Preserve scroll and selection only when the query identity remains compatible. Switching to a different filter can make the previous position meaningless; retaining it blindly may show an empty viewport and appear broken.
Sources
React: Profiler documents render measurement.
MDN: Performance APIs documents browser performance tools.
MDN: table element documents table semantics.
About the Author

Kamruz
Full Stack Developer
A passionate developer who loves creating modern web applications and sharing knowledge with the community.
More About Me