diff --git a/content/img/react-redux-after-flamegraph.png b/content/img/react-redux-after-flamegraph.png new file mode 100644 index 0000000..3b78162 --- /dev/null +++ b/content/img/react-redux-after-flamegraph.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a66c777475fdeac0d980f2029f1395c1cd0835079aac515e63ca68861da9c6f4 +size 134415 diff --git a/content/img/react-redux-component-hooks.png b/content/img/react-redux-component-hooks.png new file mode 100644 index 0000000..78e67a9 --- /dev/null +++ b/content/img/react-redux-component-hooks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f4b0e11e40d4e6b684cb2583bde5d6d6a832e75b2dacd05afa9bbdb32792627 +size 66856 diff --git a/content/img/react-redux-rerender-flamegraph.png b/content/img/react-redux-rerender-flamegraph.png new file mode 100644 index 0000000..3748258 --- /dev/null +++ b/content/img/react-redux-rerender-flamegraph.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1ea7fd467399164fec9af66956e4714f94a2dfd351b2b0df240ef30064d51db +size 166123 diff --git a/content/posts/2022.09.18.solving-react-redux-triggering-too-many-re-renders.md b/content/posts/2022.09.18.solving-react-redux-triggering-too-many-re-renders.md new file mode 100644 index 0000000..2a52748 --- /dev/null +++ b/content/posts/2022.09.18.solving-react-redux-triggering-too-many-re-renders.md @@ -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. + +{{}} + +{{}} + +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( + params: RequestParams, + 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, Right extends Record>(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. diff --git a/content/vid/react-redux-causes-re-renders.mp4 b/content/vid/react-redux-causes-re-renders.mp4 new file mode 100644 index 0000000..f53b442 --- /dev/null +++ b/content/vid/react-redux-causes-re-renders.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e492c2ba4d8bd80fc96adc7c0a8ba856a73f43dd872f9e8e18228d2edf33d7ad +size 1140070 diff --git a/themes/catafalque b/themes/catafalque index f8e3226..dab69e3 160000 --- a/themes/catafalque +++ b/themes/catafalque @@ -1 +1 @@ -Subproject commit f8e32261fa2aa4a17a83bb8fbde8f0b94341365b +Subproject commit dab69e3307e99d7dc77005e66a363abb36269e90