Windowing

NOTE: We're going to be using a library called @tanstack/react-virtual. Some of you may have used this before and others may not have used it or anything else like it. I normally like to build up to abstractions like this one by building a simple version of the abstraction ourselves, but that would be a workshop entirely to itself! Implementing this is nontrivial, so try to focus on the concepts even though we're using a library.
As we learned in the last exercise, React is really optimized at updating the DOM during the commit phase.
Unfortunately, there's not much React can do if you simply need to make huge updates to the DOM. And as fast as React is in the reconciliation phase, if it has to do that for tens of thousands of elements that's going to take some time ("perf death by a thousand cuts"). In addition, our own code that runs during the "render" phase may be fast, but if you have to do that tens of thousands of times, you're going to have a hard time being fast on low-end devices.
There's no UI that reveals these problems more than dataviz, grids, tables, and lists with lots of data. There's only so much you can do before we have to conclude that we're simply running too much code (or running the same small amount of code too many times).
But here's the trick. Often you don't need to actually display tens of thousands of list items, table cells, or data points to users. Users can't process it all anyway. So if that content isn't displayed, then you can kinda cheat by doing some "lazy" just-in-time rendering.
So let's say you had a grid of data that rendered 100 columns and had 5000 rows. Do you really need to render all 500000 cells for the user all at once? They certainly won't see or be able to interact with all of that information at once. You'll only display a "window" of 10 columns by 20 rows (so 200 cells for example), and the rest you can delay rendering until the user starts scrolling around the grid.
Maybe you can render a few cells outside the view just in case they're a really fast scroller. In any case, you'll save yourself a LOT of computational power by doing this "lazy" just-in-time rendering.
This is a concept called "windowing" and in some cases it can really speed up your components that render lots of data. There are various libraries in the React ecosystem for solving this problem. My personal favorite is called @tanstack/react-virtual. Here's an example of how you would adapt a list to use @tanstack/react-virtual's useVirtualizer hook:
// before
function MyListOfData({ items }) {
	return (
		<ul style={{ height: 300 }}>
			{items.map((item) => (
				<li key={item.id}>{item.name}</li>
			))}
		</ul>
	)
}
// after
function MyListOfData({ items }) {
	const parentRef = useRef<HTMLUListElement>(null)

	const rowVirtualizer = useVirtualizer({
		count: cities.length,
		getScrollElement: () => parentRef.current,
		estimateSize: () => 20,
	})

	return (
		<ul ref={parentRef} style={{ position: 'relative', height: 300 }}>
			<li style={{ height: `${rowVirtualizer.getTotalSize()}px` }} />
			{rowVirtualizer.getVirtualItems().map((virtualItem) => {
				const item = items[index]
				if (!item) return null
				const { index, key, size, start } = virtualItem
				return (
					<li
						key={key}
						style={{
							position: 'absolute',
							top: 0,
							left: 0,
							width: '100%',
							height: `${size}px`,
							transform: `translateY(${start}px)`,
						}}
					>
						{item.name}
					</li>
				)
			})}
		</ul>
	)
}
In summary, rather than iterating over all the items in your list, you simply tell useVirtualizer how many rows are in your list, give it a callback that it can use to determine what size they each should be, and then it will give you back getVirtualItems() and a totalSize which you can then use to only render the items the user should be able to see within the window.
@tanstack/react-virtual has some really awesome capabilities for all sorts of lists (including variable sizes and grids). Definitely give it a look to speed up your lists.