Optimizing large table rendering in React
I recently had an issue working on Wobaka.com where the contacts table were taking too long time to re-render. What happened was that when more than 100 contacts were rendered, the UI felt laggy and sometimes froze when changing filters (free text, lead status, etc). I wasn't happy with this at all.
Let's digg into some potential factors to why this is. Oh, and I use one app state for the whole app passed through the root (it's a similar setup to how Redux works).
This is what the table looks like:
Factors to consider
- Re-calculating what contacts should be displayed for the filters on every state change multiple times for different components
- Updating the filter on every key stroke for text input
- Rendering a lot of rows
- Rendering rich components for each row
- Have proper keys defined
After wrangling around code for an hour or so it turned out that (3) by far had the most impact, but also (1) and especially (2). While rendering rich components weren't really a big issue here, compared to the others. And since I used id's as keys that wasn't a problem either.
Finding solutions
I started out working on the issue with rendering a lot of items. I decided to implement a modified version of infinite scroll that does what you expect (load more content when you scroll), but also resets that number of rows whenever a filter change. Since I know that given a filter change, you're on the top of the list and won't really notice a change in items rendered. Nice.
For the issue with re-calculating contacts I figured that I could memoize that function given (contacts, filter) paramters. This gave good enough performance wins.
Lastly, I added debouncing to the text input which made it feel reaaallyy smooth instead of kind-of laggy.
Is it faster now?
The table now renders about 10x faster, the text input is butter-smooth and I'm very happy. I really think making a product that you use yourself every day is such a huge advantage since you'll spot all the small details.
To summarize:
- Render only content that you need since the DOM is slow
- Debounce text inputs and other frequent events
- Memoize heavy lifting functions if the call pattern is repetitive
Now I'll go have myself a coffee and head for some rock climbing.
Sidenote: I'm actually using ClojureScript with a React library called Rum. But it's all the same principles :).