Lists, virtualization and code splitting in React
Render 100,000 rows smoothly by rendering only the visible ones (TanStack Virtual, react-window), split code with React.lazy and Suspense, preload chunks on hover, and find what makes the bundle big.
Comes up in: React, Frontend system design, Machine coding
Learn#
Asha’s transactions page renders 100,000 rows. It takes 9 seconds to open, and scrolling stutters. The browser holds 100,000 DOM rows, but the screen shows 15.
Virtualization renders only the rows near the viewport. A tall empty container keeps the scrollbar the right size, and the visible rows are moved into place as the user scrolls. Libraries such as TanStack Virtual and react-window do the math (you will write it in the practice).
Code splitting is the same idea for JavaScript: send code only when it is needed. React.lazy wraps a dynamic import() (JS.10.03), which is lazy loading, and Suspense shows a fallback while the chunk downloads:
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register(); // a DOM for Node; in a browser you already have one
const { createElement: h, lazy, Suspense } = await import("react");
const { createRoot } = await import("react-dom/client");
let downloads = 0;
// In a real app: lazy(() => import("./Chart.jsx")) — the bundler puts Chart in its own chunk.
const Chart = lazy(async () => {
downloads++;
await new Promise((r) => setTimeout(r, 50)); // the chunk downloading
return { default: ({ points }) => h("svg", { "data-points": points.length }) };
});
const box = document.createElement("div");
const root = createRoot(box);
const page = (showChart) => h("main", null, h("h1", null, "Sales"),
showChart && h(Suspense, { fallback: h("p", null, "Loading chart…") }, h(Chart, { points: [3, 5, 8] })));
root.render(page(false));
await new Promise((r) => setTimeout(r, 20));
console.log("before opening the report:", box.innerHTML, "| downloads:", downloads);
root.render(page(true));
await new Promise((r) => setTimeout(r, 20));
console.log("while loading:", box.querySelector("p")?.textContent);
await new Promise((r) => setTimeout(r, 600));
console.log("after:", box.querySelector("svg")?.getAttribute("data-points"), "points | downloads:", downloads);
root.unmount();Output (verified)
before opening the report: <main><h1>Sales</h1></main> | downloads: 0 while loading: Loading chart… after: 3 points | downloads: 1
State
- count 100,000
- visible ≈ 15
Page
empty
100,000 transactions, each 40 px tall. The viewport shows 600 px, so about 15 rows are visible.
Read the steps as text
- 100,000 transactions, each 40 px tall. The viewport shows 600 px, so about 15 rows are visible.
- A tall empty container (4,000,000 px) makes the scrollbar correct. Only rows 0–19 (with overscan) exist in the DOM.
- The user scrolls to 40,000 px. The range becomes rows 995–1,020; the old rows are removed or reused.
- The DOM never holds more than about 25 rows, so scrolling and typing stay fast.
Predict#
Predict first
You virtualize a list. The user presses Ctrl+F to find a transaction that is far down. What happens?
Virtualized rows do not exist until scrolled to, so in-page search, and some assistive technology, cannot reach them. Offer your own search or filter, and keep lists that need native find under a few thousand rows, or use content-visibility: auto instead.
Interview angle#
Say it like this — about 30 seconds
“For a list of 100,000 items, I virtualize: render only the rows in and near the viewport inside a container with the full height, using a library such as TanStack Virtual. For bundle size, I split by route and by heavy features with React.lazy and Suspense, preload chunks on hover or when the browser is idle, and run a bundle analyzer to find large dependencies. Then I replace heavy libraries, import only what I use, and drop unneeded polyfills.”
Follow-up How would you render a list of 100,000 items?
Follow-up How do you reduce a React app's bundle size?
Follow-up How do you avoid a delay when the user clicks a lazy route?
Practice#
Guided
Write the core math of a virtualized list: which rows to render and where to place them.
Done when
- Partly visible rows count as visible, and overscan adds rows on both sides within bounds.
offsetYandtotalHeightare correct.- Empty lists return an empty range.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
What is list virtualization?
Rendering only the rows near the viewport, inside a full-height container.
What does React.lazy wrap?
A dynamic import() of a component module.
What shows while a lazy component downloads?
The nearest Suspense boundary's fallback.
A downside of virtualization?
Off-screen rows are not in the DOM, so browser find cannot reach them.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Overscan renders a few extra rows above and below, so fast scrolls do not show blanks. Variable-height rows need measuring; libraries handle it with estimates plus measurement.
Check yourself: What is overscan?
Extra rows rendered just outside the viewport, so fast scrolling shows no blank gaps.
How it works underneathSenior rounds
Check yourself: Where should most code splitting start?
At routes, then heavy components such as editors and charts.
Edge cases and trapsWhere people slip
lazy() at module level; inside a component it creates a new type each render. Keep a reference to the import function to preload on hover: const load = () => import("./Chart").Check yourself: Why must lazy() be called outside the component?
Inside, it creates a new component type every render, which remounts and refetches.
In productionStaff-level answers
Check yourself: Name a tool to see what is in your bundle.
A bundle analyzer, such as rollup-plugin-visualizer or source-map-explorer.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- How do you reduce a React app's bundle size?
- How would you render a list of 100,000 items?