A React team proposes virtualizing every long list in the product. As the reviewer, how do you decide which lists genuinely warrant windowing, and what would you push back on?
answer
- measure the real distribution first
- cost is rows times cost per row
- shorter lists beat faster lists
- it changes what the page is
- one shared implementation, not nine
basics
~20 sDecide from measured production data: the p95 row count, the DOM and render cost of a single row, and whether scrolling actually drops frames. Windowing earns its permanent complexity and accessibility cost only for lists that are genuinely large and cannot be shortened.
solid answer
~50 sI would ask for evidence before architecture. Three numbers decide it: the p95 and p99 row counts in production, the DOM node and render cost of one row, and whether recorded scrolls actually drop frames. A thousand cheap rows often scroll fine; two hundred rows with a chart each do not, and windowing is the wrong fix for the second case — cutting per-row cost is. I would also weigh alternatives that are cheaper to own: server-side pagination, filters that make long lists rare, a capped result set with a prompt to refine the search. Where we do virtualize, I want the cost acknowledged — find-in-page and print change behaviour, keyboard focus and ARIA counts need explicit handling, variable heights bring measurement bugs. My pushback is on "every list": that is a policy applied without measurement, and it taxes small lists with complexity they will never repay.
go deeper
Know that virtualization is for genuinely large lists and is not a default; a short list gains nothing from it and inherits real complexity.
Be able to compare it against pagination, filtering and simply making the row cheaper, and explain why total DOM nodes matter more than row count on its own.
Bring evidence to the decision — recorded scroll profiles, node counts, production row distributions — and state which interactions each candidate list must keep working.
Set the threshold and the standard: one shared implementation, a documented cost in accessibility and browser features, and a clear position that a blanket virtualize-everything policy is a measurement failure.
## Start with the number nobody has The first question is not technical: *how long are these lists in production, really?* Teams virtualize based on the worst case they can imagine rather than the distribution they actually serve. Instrument it — sample the rendered row count — and look at p50, p95 and p99. A list whose p95 is 60 rows and whose p99 is 4,000 has a tail problem, not a list problem, and a cap plus a filter may serve better than windowing every instance. ## The cost model has two factors, not one Render cost is roughly *rows times cost-per-row*. Windowing attacks the first factor only. That yields a simple triage: - **Many rows, cheap rows** (a plain text log, an id list): the classic virtualization case. Thousands of rows, a handful of nodes each. Windowing turns an unusable page into a fast one. - **Few rows, expensive rows** (200 cards, each with a chart, an avatar and three tooltips): windowing helps far less than it looks, because 20 expensive rows is still expensive. Fix the row instead — fewer nodes, no per-row observers, defer secondary content. - **Many rows, expensive rows**: do both, and do the row work first, since it also improves the virtualized case. - **Few rows, cheap rows**: do nothing. This is where a blanket policy does real damage. A useful proxy is total DOM nodes rather than row count. Three hundred rows at forty nodes each is 12,000 nodes; three thousand rows at four nodes each is the same. The browser cares about the product of the two. ## The alternatives that are cheaper to own Before virtualizing, ask whether the list should be that long at all: - **Server-side pagination or cursor-based loading** solves the payload as well as the DOM, and keeps every browser feature intact. - **Filtering and search-first interfaces** make long lists rare instead of fast. A user scrolling 10,000 rows to find one record is a design failure that windowing makes tolerable rather than fixes. - **A hard cap with a clear message** — showing the first 500 of 12,400, refine your filters — is honest, trivial to build, and often what users actually want. - **Cheaper rows.** Flattening markup and deleting per-row wrappers frequently buys more than windowing would, with no ongoing cost. - **Collapsed detail.** Clamping rows to a fixed height and moving overflow into an expandable panel keeps rows uniform, which also makes any future virtualization dramatically simpler. ## The recurring costs to name out loud Virtualization is not a one-time change; it is a constraint every subsequent feature must respect: - Browser features that walk the DOM — find-in-page, print, select-all — degrade permanently, and accessibility needs deliberate work such as declared counts and managed focus. - Sticky group headers, expandable rows, inline editing, drag-and-drop reordering and row animations all get harder, because rows mount and unmount underneath them. - Variable heights bring measurement, invalidation and scroll-anchoring bugs that recur whenever content shapes change. - Tests get harder: assertions about a row outside the window fail, so suites need scroll helpers or data-level assertions. - Nested virtualization — a windowed list inside windowed rows — is a strong smell; two scroll containers and two measurement caches interacting rarely pays off. ## What I would ask for in review 1. A profile: a recorded scroll with dropped frames, commit durations, and the total DOM node count for the worst real page. 2. The production row-count distribution, not a hypothetical. 3. What was tried on the row itself first. 4. For each proposed list, the interactions it must keep supporting — keyboard, selection, print, deep link — and who is doing that work. 5. A single shared, reviewed list component if we adopt it, so measurement, ARIA and focus handling are implemented once rather than nine times with nine sets of bugs. ## The framing that lands Virtualization is one of the few frontend optimizations that changes what the page *is*, not merely how fast it is. That makes it a product and accessibility decision as much as a performance one. "Every long list" is a policy; the right answer is a threshold backed by measurement, a shared implementation, and a documented list of what a virtualized list must ship with.
- A page shows 200 rows and scrolling is janky. Is virtualization the right first move?Usually not. At 200 rows the problem is almost always per-row cost — deep markup, a chart or observer per row, expensive formatting — and windowing to 25 rows still leaves 25 expensive rows. Profile one row's render and node count first; flattening the row often fixes it outright and costs nothing in accessibility, printing or test complexity.
- What would make you reject a proposal to virtualize a list nested inside already-virtualized rows?Two scroll containers and two measurement caches interacting is a maintenance trap: outer rows change height as inner content measures, scroll anchoring fights itself, and keyboard navigation across the boundary is close to unsolvable. I would push for collapsing the inner list to a fixed-height summary with a detail route, which removes the nesting entirely.
- If you do adopt windowing across the product, what would you standardise?One shared list component that owns measurement, overscan defaults, roving-tabindex focus handling, ARIA counts, scroll-to-index and an unwindowed print path, plus a written checklist for anything rendered inside a row. The failure mode of ad-hoc adoption is nine implementations with nine different focus bugs and no consistent accessibility story.
saying these in an interview costs you the question
- Treats row count alone as the trigger, ignoring per-row cost
- Adopts virtualization without profiling anything
- Assumes windowing also reduces the data payload
- Ignores the permanent accessibility and print costs
- Nests virtualized lists inside virtualized rows