Skip to content

Commit

Permalink
finish workshop instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Mar 22, 2024
1 parent ef1b523 commit e5097fe
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 102 deletions.
40 changes: 0 additions & 40 deletions exercises/06.rerenders/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -161,43 +161,3 @@ really wanted to take advantage of `memo` here, we'd have to wrap `increment` in
It's much better to use `memo` more intentionally and further, there are other
things you can do to reduce the amount of unnecessary rerenders throughout your
application (some of which we've done already).

### 1. 💯 Use a custom comparator function

[Production deploy](https://react-performance.netlify.app/isolated/final/03.extra-1.js)

You'll notice that as you hover over the elements in the list (or click the
input and press the down and up arrow) the `highlightedIndex` changes. This prop
changes for all the `ListItem` components, but it doesn't mean that they all
need DOM updates. The only ListItems that need a DOM update are 1) the old
highlighted item, and 2) the new highlighted item.

Luckily for us, `memo` accepts a second argument which is a custom compare
function that allows us to compare the props and return `true` if rendering the
component again is **unnecessary** and `false` if it is necessary.

See if you can figure out how to use that function to make it so changing the
highlighted index only rerenders the components that need the change.

> NOTE: You can do the same for `selectedItem`, though that one may be trickier
> to test and I tried it and couldn't get it to work 😅 I spent 20 minutes on it
> before giving up! Maybe you can figure it out though. This is why these are
> **OPTIMIZATIONS** and not to be applied in every case. They're hard to get
> right and easy to mess up and create bugs!
### 2. 💯 pass only primitive values

[Production deploy](https://react-performance.netlify.app/isolated/final/03.extra-2.js)

Wouldn't it be even better to not have to provide our custom memoization
comparator function and still get the perf gains? Definitely! So an alternative
approach is to pass the pre-computed values for `isSelected` and `isHighlighted`
to our `ListItem`. That way they are primitive values and we can take advantage
of React's built-in comparison function and remove ours altogether.

The additional benefit to this is when you select an item, only the selected
item needs a rerender.

So for this extra credit, try to accept an `isSelected` and `isHighlighted` prop
to `ListItem` so you don't have to pass the frequently changing values for
`selectedItem` and `highlightedIndex`.
20 changes: 20 additions & 0 deletions exercises/07.windowing/01.problem.virtualizer/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,23 @@

Hover over one of the items with CPU 6x throttle and it's crazy slow despite
only re-rendering the ListItem

👨‍💼 I've noticed that we're only rendering the top 500 matches and our users are
frustrated because they like scrolling for eternity. We tried to explain to
them about React's reconciliation and commit phases, but they just smiled, gave
us a high five and said "You're smart! I'm sure you can do this!" 🤷‍♂️

🧝‍♂️ I've <PrevDiffLink>removed the `.slice(0, 500)`</PrevDiffLink> and now we're
rendering all the items. Even without throttling your CPU, you should notice a
serious performance problem here any time there's any render of any kind. You'll
notice this especially when React needs to render all the items. For example,
you can type a search query, then clear it and you'll definitely notice it then.

Go ahead and try doing some profiling if you like, but I think you know what you
need to do. You need to window this stuff! Run less code, and speed up your
component.

👨‍💼 Ok, you should be good to go! Note that 💰 Marty the Money Bag will be
holding your hand for some stuff a tiny bit for things that are specific to how
you integrate `downshift` and `@tanstack/react-virtual`. 🐨 Kody the Koala will
be there to help you know what changes you need to make. Good luck!
3 changes: 3 additions & 0 deletions exercises/07.windowing/01.solution.virtualizer/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Virtualizer

👨‍💼 Great job! Even with returning all of the results and slowing down the CPU to
6x, we're still getting great performance on this list. Well done 👏👏
9 changes: 9 additions & 0 deletions exercises/07.windowing/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
# Windowing

👨‍💼 You did it! Great work on improving the performance of this component!

🦉 One thing you should know about virtualization is that ⌘+F (or on
Linux/Windows Ctrl+F) will not work out of the box. This is because the browser
doesn't know about the virtualized items. You can implement this yourself, but
it's a bit more complex (you've probably seen other apps do this). Hopefully
one day the platform will have a better primitive for this type of thing. The
web is always evolving!
90 changes: 28 additions & 62 deletions exercises/07.windowing/README.mdx
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
# Windowing

# Window large lists with react-virtual

## 📝 Your Notes

Elaborate on your learnings here in `src/exercise/04.md`

> NOTE: We're going to be using a library called `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.
## Background
<callout-info class="aside">
NOTE: We're going to be using a library called
[`@tanstack/react-virtual`](https://npm.im/@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.
</callout-info>

As we learned in the last exercise, React is really optimized at updating the
DOM during the commit phase.
Expand All @@ -31,8 +26,9 @@ 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. So if that content isn't
displayed, then you can kinda cheat by doing some "lazy" just-in-time rendering.
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
Expand All @@ -48,8 +44,8 @@ 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
`react-virtual`. Here's an example of how you would adapt a list to use
`react-virtual`'s `useVirtual` hook:
`@tanstack/react-virtual`. Here's an example of how you would adapt a list to
use `@tanstack/react-virtual`'s `useVirtualizer` hook:

```tsx
// before
Expand All @@ -68,27 +64,27 @@ function MyListOfData({ items }) {
// after
function MyListOfData({ items }) {
const listRef = React.useRef()
const rowVirtualizer = useVirtual({
const rowVirtualizer = useVirtualizer({
size: items.length,
parentRef: listRef,
estimateSize: React.useCallback(() => 20, []),
overscan: 10,
getScrollElement: () => listRef.current,
estimateSize: () => 20,
})

return (
<ul ref={listRef} style={{ position: 'relative', height: 300 }}>
<li style={{ height: rowVirtualizer.totalSize }} />
{rowVirtualizer.virtualItems.map(({ index, size, start }) => {
<li style={{ height: `${rowVirtualizer.getTotalSize()}px` }} />
{rowVirtualizer.getVirtualItems().map(virtualItem => {
const { index, key, size, start } = virtualItem
const item = items[index]
return (
<li
key={item.id}
key={key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: size,
height: `${size}px`,
transform: `translateY(${start}px)`,
}}
>
Expand All @@ -102,41 +98,11 @@ function MyListOfData({ items }) {
```

In summary, rather than iterating over all the items in your list, you simply
tell `useVirtual` 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
`virtualItems` and a `totalSize` which you can then use to only render the items
the user should be able to see within the window.
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.

[react-virtual](https://github.com/tannerlinsley/react-virtual) has some really
awesome capabilities for all sorts of lists (including variable sizes and
[@tanstack/react-virtual](https://npm.im/@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.

## Exercise

Production deploys:

- [Exercise](https://react-performance.netlify.app/isolated/exercise/04.js)
- [Final](https://react-performance.netlify.app/isolated/final/04.js)

👨‍💼 The product manager noticed that we're only rendering the top 100 matches and
they're frustrated because they like scrolling for eternity. We tried to explain
to them about React's reconciliation and commit phases, but they just smiled,
gave us a high five and said "You're smart! I'm sure you can do this!"

So in this exercise, we've removed the `items.slice(0, 100)` and now we're
rendering all the items. Even without throttling your CPU, you should notice a
serious performance problem here any time there's any render of any kind. Go
ahead and try doing some profiling if you like, but I think you know what you
need to do. You need to window this stuff! Run less code, and speed up your
component.

Note that 💰 Marty the Money Bag has done a tiny bit of work for you that's
specific to how you integrate Downshift and react-virtual. He's done a good job
documenting what he's done so you can read about the changes he made if you
want. 🐨 Kody the Koala will be there to help you know what changes you need to
make. Good luck!

## 🦉 Feedback

Fill out
[the feedback form](https://ws.kcd.im/?ws=React%20Performance%20%E2%9A%A1&e=04%3A%20Window%20large%20lists%20with%20react-virtual&em=).
5 changes: 5 additions & 0 deletions exercises/FINISHED.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ There's still so much more we can do to improve performance of our applications,
but you've now been given the tools to analyze and improve your own
applications.

<callout-danger class="important">
Remember: measure first, then optimize, then measure again. Don't make your
code more complex for no reason.
</callout-danger>

🦉 One last thing I want to note is that everything we've done in this workshop
involved client-rendered applications. There's a ceiling of performance
improvements you hit with client-rendered applications. To break through that
Expand Down

0 comments on commit e5097fe

Please sign in to comment.