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.
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
- Measure. Network and Performance tabs first, then the React Profiler.
- Reduce the bundle: code split routes and heavy libraries.
- Virtualise long lists.
- Move state down, or lift content up, to shrink what re-renders.
- Only then apply
memo,useMemoanduseCallback, 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.