Blog Home RSS Kelvin Jackson

Optimizing React UIs: Avoiding Unnecessary Re-Renders


One of the main causes of sluggishness in a React UI is unnecessary re-rendering. If you aren't careful, it is easy to write code that will render your entire app — or a very large chunk of it at least — every time any bit of state changes. While this may be a necessary evil during the development process when the app is still only a half-finished skeleton, no programmer who feels pride in their work should ever subject their users to a slow UI: for production, we can and must do better.

The first rule of avoiding unnecessary re-renders is to not give React a reason to re-render if you can avoid it. Always bear in mind that React re-renders a component if the identity of any prop has changed — that is, shallow equality. This is an intentional optimization on the part of the React devs to avoid the performance cost of deep comparisons, but it can easily trip up an inexperienced developer, especially in the functional programming -oriented world of React where we learn to create new objects rather than mutating existing ones.

One common pitfall is anonymous functions — it is easy to forget that they too are objects, and that an anonymous function definition creates a new object every time control passes through that section of the program. If you are passing a function to a component as a prop, and that function definition cannot be lifted out of the component body, you can use useCallback() to ensure that it is only reinstantiated when truly necessary:

import { useCallback } from "react"; // ... const SomeComponent = ({ someProp }) => ( <InnerComponent propThatTakesFunction={ useCallback(x => { // useCallback() will store this function, and return the same // instance every time useCallback() is called with the same // value of someProp. doSomethingWith(x, someProp); }, [someProp]) } /> );

Also look for places where you might be passing multiple items to a component by wrapping them in an object constructor and passing that as a prop — this will also cause the component to re-render every time the parent component is rendered, even if those values have not changed. Instead, you should pass each item in individually. Remember that you can use the spread operator (...) to pass every field of an object to a component as a separate prop, so you don't have to individually access every field of a large object if you still want to group items together.

Note that you don't have to make sure that every single component is rendered only the bare minimum number of times — often that isn't worth the effort. If your UI is slow, use a profiler or carefully-placed console.log() calls to figure out where most of the cycles are being wasted, and then target your first optimizations to ensure that large, unrelated parts of the UI aren't being rerendered at every occurrance of a common action, such as a keystroke. In a very simple app (or part of an app), you can get away with extra re-renders, but in a complex UI, re-rendering everything for every button press will result in a noticeable slowdown.

Last but not least: preventing re-renders in React is unfortunately an all-or-nothing scenario. Even one prop that fails the shallow equality test will trigger the re-render, making it difficult to see your progress while optimizing until you have finished an entire component. As you gain experience with React, though, you will start to have a sense of what to do and not to do to avoid major performance problems. As with many aspects of software development, React optimization seems difficult and counterintuitive until you've done it a few times, at which point it begins to become second nature.