Add Solving React Redux triggering too many re-renders
This commit is contained in:
parent
bcf06df049
commit
6606e5751d
BIN
content/img/react-redux-after-flamegraph.png
(Stored with Git LFS)
Normal file
BIN
content/img/react-redux-after-flamegraph.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
content/img/react-redux-component-hooks.png
(Stored with Git LFS)
Normal file
BIN
content/img/react-redux-component-hooks.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
content/img/react-redux-rerender-flamegraph.png
(Stored with Git LFS)
Normal file
BIN
content/img/react-redux-rerender-flamegraph.png
(Stored with Git LFS)
Normal file
Binary file not shown.
|
@ -0,0 +1,111 @@
|
|||
---
|
||||
title: "Solving React Redux Triggering Too Many Re-Renders"
|
||||
date: 2022-09-18T18:13:31-04:00
|
||||
toc: false
|
||||
images:
|
||||
tags:
|
||||
- javascript
|
||||
- typescript
|
||||
- react
|
||||
- web
|
||||
- dev
|
||||
---
|
||||
|
||||
This might be obvious for some, but I was struggling with a performance issue in
|
||||
[Bulgur Cloud](/portfolio/#bulgur-cloud), my React (well, React Native) based
|
||||
web application. Bulgur Cloud is an app like Google Drive or NextCloud, and one
|
||||
of the features is that you can upload files. But I noticed that the page would
|
||||
slow down to a crawl and my computers fans would spin up during uploads. It
|
||||
can't be that expensive to display a progress bar for an upload, so let's figure
|
||||
out what's happening.
|
||||
|
||||
As the first step, I installed the "React Developer Tools" extension in my
|
||||
browser and enabled the "Highlight updates when components render" option. Then
|
||||
I used the network tab to add a throttle so I could see the upload happen more
|
||||
slowly, and started another upload.
|
||||
|
||||
{{<raw>}}
|
||||
<video controls width="100%">
|
||||
<source src="/vid/react-redux-causes-re-renders.mp4" type="video/mp4">
|
||||
<p>A video showing a progress bar slowly increasing. As the progress bar goes up, the entire screen flashes with blue borders.</p>
|
||||
</video>
|
||||
{{</raw>}}
|
||||
|
||||
Pretty much the entire screen flashes every time the progress bar goes up.
|
||||
Something **is** causing unnecessary re-renders! The next step in my diagnosis
|
||||
was to record a profile of the upload process with the react developer tools. I
|
||||
enable the option "Record why each component rendered when profiling", then run
|
||||
the profiler as I upload another file.
|
||||
|
||||
![A flame graph with many elements highlighted in blue.](/img/react-redux-rerender-flamegraph.png)
|
||||
|
||||
Walking through the commits in the flame graph (the part that says "select next
|
||||
commit" in the screenshot, top right), I can see this weird jagged pattern that
|
||||
repeats through the upload process. Selecting on of the tall commits again
|
||||
confirms that pretty much everything had to be re-rendered. Hovering over the
|
||||
items being re-rendered shows me that the items being re-rendered are my folder
|
||||
listings, the rows representing files. It also tells me why they re-rendered:
|
||||
"Hook 3 changed".
|
||||
|
||||
Next step is to switch over to the components feature of the react developer
|
||||
tools, and take a look at `FolderList`. Once I select it, I get a list of the
|
||||
hooks that it uses. Keep expanding the tree of hooks, and the hook numbers are
|
||||
revealed.
|
||||
|
||||
![A tree view displaying hooks for a component named FolderList](/img/react-redux-component-hooks.png)
|
||||
|
||||
The hook names here seem to be the names in the source code minus the `use`
|
||||
part, so my `useFetch` hook becomes `Fetch`. They are in a tree view since hooks
|
||||
can call other hooks inside them. Following the tree, hook 3 is `State`, and is
|
||||
located under `Selector` which is a React Redux hook `useSelector`. At this
|
||||
point things become clearer: I'm using redux to store the upload progress, and
|
||||
every time I update the progress it causes everything to re-render. And all of
|
||||
this is being caused through my fetch hook. Let's look at the code for that:
|
||||
|
||||
```ts
|
||||
export function useFetch<D, R>(
|
||||
params: RequestParams<D>,
|
||||
swrConfig?: SWRConfiguration,
|
||||
) {
|
||||
// useAppSelector is useSelector from react redux,
|
||||
// just a wrapper to use my app types
|
||||
const { access_token, site } = useAppSelector((selector) =>
|
||||
// pick is same as Lodash's _.pick
|
||||
pick(selector.auth, "access_token", "site"),
|
||||
);
|
||||
// ...
|
||||
```
|
||||
|
||||
I realized the issue once I had tracked it down to here! The state selector is
|
||||
using the `pick` function to extract values out of an object. React Redux checks
|
||||
if the value selected by the selector has changed to decide if things need to be
|
||||
re-rendered, but it uses a basic equality comparison and not a deep equality
|
||||
check. Because `pick` keeps creating new objects, the objects are never equal to
|
||||
each other, and Redux keeps thinking that it has to re-render my object!
|
||||
|
||||
The solution luckily is easy, we can tell redux to use a custom function for
|
||||
comparison. I used a `shallowEquals` function to do a single-depth comparison of
|
||||
the objects (the object is flat so I don't need recursion).
|
||||
|
||||
```ts
|
||||
export function shallowEquals<Left extends Record<string, unknown>, Right extends Record<string, unknown>>(left: Left, right: Right) {
|
||||
if (Object.keys(left).length !== Object.keys(right).length) return false;
|
||||
for (const key of Object.keys(left)) {
|
||||
if (left[key] !== right[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ...
|
||||
const { access_token, site } = useAppSelector((selector) =>
|
||||
pick(selector.auth, "access_token", "site"),
|
||||
shallowEquals
|
||||
);
|
||||
```
|
||||
|
||||
Let's look at the profile now:
|
||||
|
||||
![A flame graph with only a tiny portion of elements highlighted.](/img/react-redux-after-flamegraph.png)
|
||||
|
||||
Much better! The only thing re-rendering now is the progress bar itself, which
|
||||
is ideal.
|
BIN
content/vid/react-redux-causes-re-renders.mp4
(Stored with Git LFS)
Normal file
BIN
content/vid/react-redux-causes-re-renders.mp4
(Stored with Git LFS)
Normal file
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit f8e32261fa2aa4a17a83bb8fbde8f0b94341365b
|
||||
Subproject commit dab69e3307e99d7dc77005e66a363abb36269e90
|
Loading…
Reference in a new issue