Skip to content

Instantly share code, notes, and snippets.

@narthollis
Created February 28, 2017 09:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save narthollis/26b59d8844c6aabdb391e50febfa2671 to your computer and use it in GitHub Desktop.
Save narthollis/26b59d8844c6aabdb391e50febfa2671 to your computer and use it in GitHub Desktop.

React + Redux Application Performance Investigation

Our application was having issues when rendering a list with approximately 150 items. Each item in the list is rendered into a number of columns determined by configuration. The columns range from being a glorified print string, to reasonably complex selection logic and time since presentations.

The performance issues mainly presented themselves when the application would poll for data - which in this case would result in retrieving the full data set for all views in the application. In future we hope to move to a diff based poll or eventing but have not yet had time to implement the backend.

Fix attempt 0 - Reference Equality

The very first attempt at fixing this problem was to ensure that we were only replacing items in our store when the item changed. To facilitate this, we updated our API to return either a hash of the object, or an object version. This hash or version was then used a cheap comparison operator, along with the items own ID, to determine if the previous item could be reused or not.

That is, if nothing has changed reuse the previous object and thereby maintain reference equality for it. (And absolutely no mutation of the store)

Putting in this change reduced the number of column components that React devtools was highlighting as having rendered. However this did not fix the overall performance problem - the application (and browser tab) would still hang for 10 or so seconds every time the poll would run.

First Stage - Try Turning it off and on again

When we next took time to look at this problem, we started with the age old IT solution of turning it on and off again; which in this case meant commenting out code section that felt like that could be contributing to the long load times.

Many things were tried - the additional selectors used for sorting, the sorting itself, counts and summations, and even reducing the number of columns rendered to only two of the more basic columns.

Usefully, none of this really helped at all.

Stage Two - Perf tooling

Next up was trying some perf tooling - mainly using react-addons-perf. However it turned out that the this issue was so bad that the perf addon would cause the browser to lock up until it ran out of memory and crashed.

Part Three - Loop Hunting Interlude

Due to the aforementioned issues getting perf tooling working, and the way in which it was crashing out, I started to assume there was some bad loop conditioning somewhere. I hunted all through the code base attempting to find where it could be getting stuck. This was all until a co-worker suggested trying the perf tooling on a list with fewer items in it.

Round Four - Perf tooling FTW

Selecting a filter that is known to have a reduced data set we pulled up the perf tooling again and started the real investigation. What it turned out was occurring was a massive amounts of re-rendering for just about every component.

React.PureComponent to the rescue

Safe in the knowledge that I had ensured the store was not being mutated (and that we were maintaining reference equality when appropriate) I started converting a number of our core connected components to React.PureComponent.

This provided immediate benefits, reducing the render count on one component type from something around 16 instances 250 renders down to 16 instances 42 renders. With some more tweaking and some slight selector cleanup I was able to see the next issue.

Doing too much in non-memoized selectors

There was a component with 220 instances 2600 renders. I felt like I may have finally found the real issue.

My first take was that this component too needed to become a React.PureComponent. However upon inspection I found that it already was. (I must have converted this one during attempt 0).

This lead me to take a look at it's mapStateToProps which turned out to be a large pile of code trying its best to join models that could be in a number of separate ways (the app runs on a legacy model). I now concided that I would need to write a memoized selector. This was something I was hoping to avoid as it would mean a reasonable of refactoring, without any unit tests to back me up (so far only our reducers are exhaustively tested). With this decided, I pulled in reselect and went at it.

The result of this refactor was the component was now rendered once per instance. A significant win!

Finally, batches.

I was pretty much ready to call it done at this point. The application now only hitched for about a second during the poll.

However, looking at what was left in perf, I saw the root list component being called 8 times per poll. Incidentally this was also the number of models that it gets from the store. This smelled of more than simple coincidence to me.

After some googling for how to debounce redux subscriptions (a terminology it took me a little while to discover), I found redux-batched-actions in the Redux performance FAQ. This a simple little addon for redux that lets you batch actions together so as to avoid multiple resultant subscriber calls.

This let me batch up the result of the poll, and I was able to get the list components down to a single render.

Other cleanup

In addition to all of the above I also updated the project dependencies. This included updating react-redux to 5.0.3, which has some significant performance improvements itself.

I also made a change to the polling code to only query the high change-frequency APIs every poll. I set the lower change-frequency APIs are to run every 5th poll.

Summary

Looking back, It appears there is no one problem that caused the massive slowdowns itself, but rather a number of smaller 'gotcha's that compounded to produce the overall issue.

The main take away are the following concepts that need to be taken forward into all new React+Redux applications:

  • Always use React.PureComponent (unless there is a very good reason otherwise).
  • Use memoized selectors, and use them properly.
  • When receiving data in batches, it helps to execute your Redux actions as a batch to avoid extraneous redux subscriber calls.
  • Consider moving selector calculations that don't need access to OwnProps into their own reducer. (Remember, every reducer gets every action).
  • Always use React.PureComponent (yes it's here twice intentionally)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment