Engrain by ByteRush

RCT.11.02 Mid-level· 11 minCore — asked often

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();
This example uses Node.js, so it runs in our checks rather than in your browser.

Output (verified)

before opening the report: <main><h1>Sales</h1></main> | downloads: 0
while loading: Loading chart…
after: 3 points | downloads: 1
100,000 rows, about 25 in the DOMStep 1 of 4

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
    1. 100,000 transactions, each 40 px tall. The viewport shows 600 px, so about 15 rows are visible.
    2. A tall empty container (4,000,000 px) makes the scrollbar correct. Only rows 0–19 (with overscan) exist in the DOM.
    3. The user scrolls to 40,000 px. The range becomes rows 995–1,020; the old rows are removed or reused.
    4. 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?

    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.”

    Stays on your device. Nothing is uploaded.
    Follow-up How would you render a list of 100,000 items?
    Virtualize it, ideally with server-side pagination or search too, so neither the DOM nor memory holds everything.
    Follow-up How do you reduce a React app's bundle size?
    Route-level splitting, lazy heavy components, a bundle analyzer, lighter libraries, targeted imports and fewer polyfills.
    Follow-up How do you avoid a delay when the user clicks a lazy route?
    Call the import early: on link hover or focus, or when the browser is idle, so the chunk is cached by click time.

    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.
    • offsetY and totalHeight are correct.
    • Empty lists return an empty range.
    Your solution
    Ctrl/⌘ + Enter

    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
    Split at routes first, then heavy widgets such as charts, editors and maps. A lazy component’s module is loaded once and cached Source: React: lazy.
    Check yourself: Where should most code splitting start?

    At routes, then heavy components such as editors and charts.

    Edge cases and trapsWhere people slip
    Declare 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

    Use a bundle analyzer to see what is in each chunk, then replace or split the biggest parts (QLT.01.05).
    For lists, TanStack Virtual is headless and works with any markup Source: TanStack Virtual.

    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?