React Components performance checkup — useVigilante

Param Singh
The Startup
Published in
6 min readJun 28, 2020

Ever wondered that when the project size grows with time, why my React app seems sluggish and my mac pro is heating up due to it for no apparent reason? Let me break your bubble.

There’s always a reason

The simple reason of your UI/UX being sluggish is when the browser rendering thread doesn’t get enough time to do its job such a re-calculate styles. developer.google.com has already done a commendable job in explaining this.
I’m talking about this these nasty sand-dunes.

Above is an example from a real world giant project and this is the profiling for an action of opening an expandable-collapsible accordion item which appeared to have a very cringed user experience none of us likes. It’s so bad in some cases that you app might crash due to infinite loop of re-rendering.

React app

All of the UI frameworks out there rely on some kind of change detection mechanism. In react world, it’s called reconciliation.

When you app grows in size, Reconciliation means an expensive operation specially when your app has rich user interactions with animations. You’re bound to see a jitter in the animations or scroll performance if you don’t take it seriously.

React has provided an api for overcoming such problems in the form or PureComponent or React.memo for performing a shallow equality check on the props passed to a given component and decide whether to render it or not.

With the help of it, we can detach a component and hence all of its children from unnecessary re-renders and comparisons if we use it wisely. These apis become really imperative when we have to meticulously render a super heavy component which we can’t afford to have unnecessary re-renders or when there’s a possibility of getting stuck in the infinite render-loop

Infinite Render Loop problem

After developing in React for years, it’s not uncommon to stumble upon this worth discussing issue.

For the sake of discussion, suppose I have a hook that takes in initialValue and set it to the returning weatherData when it loads asynchronously.

And the parent component uses this hook like this

And, you’re doomed! 💥

The weatherData gets set on the initial render to a new blank object, which in turn triggers a reconciliation cycle, since it’s a setState sort of operation. When it reaches WeatherComponent, it executes the line const { weatherData } = useWeather({}) which creates a new reference for the initialValue prop passed to the hook or child, which again sets the state as its present as a useEffect dependency.

This experience is bad enough for us developers and you can’t imagine your app users to go through this on prod! Most of the times, we are unaware of these issues until it starts to manifest and people start raising it bringing embarrassment and more for the product. We might not do it deliberately as we shouldn’t call setState inside useEffect but let’s be practical, it is sometimes unavoidable to do so. Other scenario is when you’re relying on some other sophisticated functions to get and pass a prop like

const data = getData(api.fetchWeather, {}) // default value as {}const { weatherData } = useWeather(data)

It could get quite frustrating to debug why the app is behaving sluggish

Solution

In order to solve the problem, we need to identify the problem first. Our example here had a clear problem with the new reference {} being created every time on a re-render, hence if we move it outside the rendering function body, this problem will be solved!

This is a small contrived example. In a large project, such issues could happen due to a lot of props or state variables being passed around. Hence, we need a way to debug what are these variables whose reference checks inequality are breaking the system.

useVigilante

useVigilante is my attempt to bring a simple plug and play React hook to debug changing props or any variable in general. It’s inspired from the amazing Why Did You Render util which works at the app level.

If we use in on our WeatherComponent above. We can see our console flooded with these messages which clearly tells us why this problems is happening.

The usage is simple as

useVigilante(<Component or Hook Name>, { <prop1>, <state1>, ... })
Weather App
Weather app main component

It gives the logs like this

It works by equating the references of the props with the new or changed ones during re-render inside useEffect and logs the ones which have changed.

Conclusion

When in doubt, always do app profiling. As Kent c Dodds explains in this article how most of the time, you don’t need memoization

MOST OF THE TIME YOU SHOULD NOT BOTHER OPTIMIZING UNNECESSARY RERENDERS

He mentions how React is fast enough so that handle these things for you.

It’s probably the a widely misunderstood statement. React sure is fast but it is “not” fast enough to handle our silly mistakes like using setState inside useEffect to mutate a state variable and re-initialising an un-memoized variable as we saw above.

We just take it for granted until it bites us back. It is important to make judicious use of these nice apis like React.memo, useMemo, useCallback etc when necessary. A good developer should be able to take a call when to use it and when not. For eg: doing this is silly

const initialValue = useMemo(() => "Initial Value", [])

since primitive values are not equated by reference. Hence, performing unnecessary memoization is debatable here. Although, in my personal opinion, if the argument is on React being fast enough, even such things shouldn’t matter then.

Nevertheless, as the app size grows, providing the best user experience becomes utmost importance and you can help it by auditing and assessing with such debug tools.

Give it a try, it’s available on npm as @mollycule/npm

Github Repo: https://github.com/paramsinghvc/vigilante

Play on Codesandbox: Link

Thank You :)

--

--

Param Singh
The Startup

Senior Front-end Engineer at AWS, Ex-Revolut, Flipkart. JS enthusiast. Cynophile. Environmentalist