Windowing
Loading "Intro to Windowing"
Run locally for transcripts
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.