Improving web application performance with React hooks

Oskar Janczak
Jit Team
Published in
7 min readJun 17, 2019

Currently, web applications are becoming more and more complex. Fifteen years ago no one thought that Javascript code would be used to build full-fledged applications. Customers have more and more complex requirements and increasingly complicated features to be introduced. Applications become progressively complicated and demanding. We, as web developers, have to deal with the optimization of these applications.

Referring to this article: https://www.websitemagazine.com/blog/5-reasonsvisitors-leave-your-website, if the response to users’ actions is longer than 1 second, users begin to notice that their actions are performed by the computer, not themselves. Our society is living faster and faster. We need to be able to deal with building efficient applications or get to know the hacks that simulate it (like spinner display during data fetching). In this article, I would like to present some tools that React offers us in new api (16) for performance management. React hooks are the latest trends in the world of web development. In recent months, I have seen hundreds of questions about this api, so I would like to discuss the tools that developers have made available to us to improve the performance of our applications.

Examples and results

Let’s look at a simple example: https://codesandbox.io/s/m4oom51yq9. This application contains 3 components. The main component of the application is App, which has its own state and renders several components several times — Counter and Factorial. The first component uses the getMulti function, which emulates the demanding operation on the page and renders the result of this function. The second Factorial component performs the getFactorial function, waits for the mocked action and returns the result.

As we can see, the application works slowly. Each re-render takes about 300ms, which is already slow. To make the results more visible, I’ve added a couple of extra elements to further slow things down. The modified code is shown here: https://codesandbox.io/s/5zxkkk8j4n. Now the render, after each click, takes about 1.5 sec. That’s way too long — after each click the application looks like it has frozen.

Take a look at how it can be fixed: https://codesandbox.io/s/0x8nl28yjw. In this first case, the longest render I observed lasted only 1.2 ms. Almost 300 times faster compared to the first version without optimisations. If this is not impressive enough, I prepared another example. In this case the optimised version renders in around 4.3 ms while the initial — unoptimised one takes 1.5s to render! See the code for yourself: https://codesandbox.io/s/yvyw9j8vqj. To sum things up, the obtained results are provided in the table below (data from React Profiler).

Obtained performance measurements from React Profiler.

How is it possible? Is this magic? No, that’s good old React all the time. To obtain such a speedup, I’ve just used a few simple tools: useMemo, memo and useCallback.

React.memo

Let’s start with the API description: React.memo(component, fn)

The best example that will show how powerful the memo is will be a simple list:

A List without React.memo.

Total re-renders of the List component without memo is 39 in this case.

The same List with React.memo.

In this case, there are 4 re-renders in total by using React.memo. So what is memo? Memo is a higher order component similar to React.PureComponent. Memo compares props of the input and re-renders it only if props, state or context are different. So React memorizes the old result and reuses it. Memo takes two arguments: component and comparison function. By default it will only shallowly compare complex objects in the props object. If you want more control over the comparison, you can provide a custom comparison function as the second argument. See: https://reactjs.org/docs/react-api.html#reactmemo for a reference.

Yet, in some cases memo will not work. For example if we pass an array by props and change the reference to this array (note that React always creates new references to variables included in Component body).

Let’s start with an obvious example of Array comparison.

Arrays comparison

Now, if we pass an array of props of the simple Test component, the memo “trick” will not help us, since the references to the “arr” will change:

Test component will be re-render since the reference to arr changes.

To ensure better understanding of the problem, notice that a small code change — moving arr outside of the App component, makes React not copy it, and thus the component is no longer re-renedered — memo works as expected (we can get the same effect by covering arr in useMemo):

Test will not be re-render, as arr is outside of the App scope.

In some situations if it’s known that the update of our component isn’t needed, we can use the second argument of React.memo — custom shouldComponentUpdate function. An example is shown here:

Example of the usage of shouldComponentUpdate argument of React.memo.

This short movie below shows the code in action. Notice when the list gets re-rendered:

The effect of using shouldComponentUpdate in a case of a simple list.

React.useCallback

Now, we turn to the description of React.useCallback(fn, deps).

without useCallback (example from https://nikgrozev.com/2019/04/07/reacts-usecallback-and-usememo-hooks-by-example/)
with useCallback (example from https://nikgrozev.com/2019/04/07/reacts-usecallback-and-usememo-hooks-by-example/)

It should be used when we would like to avoid creating new instance of a function each time. UseCallback memorize function depends on deps arguments.

React.useMemo

Finally, let us cover the third interesting function of React.useMemo(fn, deps).

without useMemo
with useMemo

This is the most valuable tool in our cases. The useMemo function returns memorized value. So React does not recompute values if deps don’t change. This optimization helps to avoid expensive calculations (like our functions) on every render.

Conclusion

Ok, that’s it. So, are those three functions mentioned in this post the answers to all of our performance problems? Obviously not, but sometimes they help. We need to be aware that in large applications useMemo may lose some of the memorized values and generate new ones. Referring to the React documentation, e.g. to free memory for offscreen components. Tools provided by developers cannot replace good application design. Memo has some bad impact on performance. This is why, we should not use it everywhere. Sometimes it’s better not to use memo like in App and Counter component (Pure App component, in our example first render takes about 1.4ms and re-render 0.1ms and with memo: first render 2ms and re-render 0.2ms). Moreover, as I showed you earlier, react re-renders component if reference to variable has changed.

Following a brilliant suggestion by Dan Abramov (https://twitter.com/dan_abramov/status/1083897065263034368) you should not use memo by default in every component. Instead, you should consider using it in very specific cases, where it really helps and you can prove this with measurements.

We also ought to remember that most of the demanding calculations should be transferred to the server side and may only be asked by us when these values are required. Of course, sometimes that’s simply impossible — such a case inspired me to write this article. In my current project I was forced to build a huge table using popular react-table library. I was not able to dig into the code of the library to increase application’s performance. That’s why I became interested in the tools mentioned above. The results I achieved surprised me enough to say that I wanted to share them with others. I hope that I brought to light some issues of React’s optimizations.

Additional notes

First of all, if our application is in advanced development stage, we should write good unit and e2e tests before attempting to improve performance. Not to mention the change in our application architecture; in this case without written tests, the refactor will not work. From my experience, I think we should not start optimization if it is unnecessary.

Optimization is a very important issue. But one of the most significant rules is: we should not repair performance if it’s unnecessary! Of course, that’s not a problem to remember about using useCallback and useMemo (but I’m begging you, remember about deps). Keep in mind two things, however. First thing, that our time is valuable, client wants to create applications faster and faster and we cannot waste our time improving performance for small cases. Second thing, that working code is the most important thing. Performance is in the second place. Bear in mind that, if you’d like to improve your application performance in the beginning: write tests, pass tests, refactor code. Then, start thinking about benefits you can obtain by using tools I introduced in this article :)

Thanks for reading!

Special thanks to Ryszard Jakielski — for inspiring me to write this article and for constructive criticism during code review.

--

--