I’m having an issue with identifying bottlenecks in render performance while working on a JSON viewer. With few elements, it performs well, but at a certain point it becomes annoyingly slow.
Checking the profiler, it seems that elements are rendering fast enough, but I’ve noticed a few issues that I’m not sure how to pursue.
- The app is a JSON viewer which allows you to expand / minimize all elements at once, as well as individual elements.
- Performance is fine with few elements, but seems to decrease dramatically as the number of elements increases.
- When profiling both my object filter method with
performance.now()as well as checking the render time in React DevTools, the figures seem okay. I could be interpreting it wrong.
- I’ve tried using
React.memo()on stateless elements (particularly the key/value which is the most frequently rendered component), but it doesn’t seem to improve the performance noticeably. Admittedly, I’m not sure if I understand the reasoning enough behind memoizing React components to implement this usefully.
- Currently, my app loads data into a parent which feeds into a component that loads the JSON tree using a recursive element.
- Loading JSON feed from URL changes the state of the parent component, which is filtered using a helper method that uses values entered into an input field.
There are two functionalities which reproduce a slow response time with (not so big) JSON documents:
- The expand all button
- The first few keypresses on a filter query
With the current implementation, both filtering and expanding all triggers a
display: none change on the child elements, and the behavior leads me to believe I’m doing something inefficiently to handle this use case.
The code is available here: https://codesandbox.io/s/react-json-view-4z348
With a production build here (not performing any better): https://csb-4z348.vercel.app/
To reproduce the issue, play around with the Expand All function (plus sign next to filter input) and some filter inputs.
Then, try loading a JSON feed with more elements (you can test on my GitHub API feed) and try filtering/expanding all. Notice the major performance hit.
What I’ve noticed
- When logging useEffect, minimizing seems to cause ~2x as many rerenders as expanding all.
- As the filter input becomes more specific, the performance (logically) improves as less elements are being rendered.
While I would appreciate a nudge in the right direction for this specific case, what I’m most curious about is how best to identify what is causing these performance issues.
I’ve looked into windowing the output, but it’s not my first choice, and I’m pretty sure I’m doing something wrong, rather than the cause being too many elements rendered.
I appreciate your time, and thank you in advance for any tips you could provide!
It seems I’ve answered my own question. The problem was a reconciliation issue due to using UUID as a key prop in my child components, which caused them to re-render every time the minimize state changed. From the docs:
Keys should be stable, predictable, and unique. Unstable keys (like
those produced by Math.random()) will cause many component instances
and DOM nodes to be unnecessarily recreated, which can cause
performance degradation and lost state in child components.
I’ll leave the steps here for anyone else who runs into this issue.
After (too long) digging around in performance profiler, I noticed that each time I minimized or expanded the elements, each child was being mounted again. After consulting Google with a more specific query, I found this blog post and realized that I was committing this flagrant performance error.
After fixing the key prop, interaction time got ~60% faster for minimize/expand all.
Finally, I memoized some other components related to the instant filter and finally it seems to be performing as well as I would like for the time being.
Thanks to anyone who took a look at this in the meantime, and I hope it’s helpful for anyone who might come across this.