Chapter 4 of 4

Rendering Large Lists

The one optimisation that reliably matters, plus splitting work at the bundle level.

Thousands of DOM nodes are slow no matter how well your components are written. Virtualisation renders only the rows currently visible, plus a small buffer.

Ten thousand items, roughly twenty DOM nodes.
import { useVirtualizer } from "@tanstack/react-virtual";

function Rows({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,
  });

  return (
    <div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={items[virtualRow.index].id}
            style={{
              position: "absolute",
              top: 0,
              transform: `translateY(${virtualRow.start}px)`,
              height: virtualRow.size,
            }}
          >
            <Row item={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Code splitting

The fastest component is one that was never downloaded. lazy and Suspense split a route or a heavy widget into its own chunk, fetched on demand.

import { lazy, Suspense } from "react";

const Analytics = lazy(() => import("./Analytics"));

<Suspense fallback={<Spinner />}>
  {showAnalytics && <Analytics />}
</Suspense>
  • Split at route boundaries first - that is where the biggest wins are.
  • Split heavy dependencies such as chart, editor and map libraries.
  • Do not split tiny components: an extra network request costs more than the bytes saved.

A checklist in priority order

  1. Measure. Network and Performance tabs first, then the React Profiler.
  2. Reduce the bundle: code split routes and heavy libraries.
  3. Virtualise long lists.
  4. Move state down, or lift content up, to shrink what re-renders.
  5. Only then apply memo, useMemo and useCallback, and verify each one helped.

Write a simple memoisation helper

Complete memoize so it caches the result per argument and only calls fn once per distinct value.