Simple ways to improve your React app performance without useMemo.

Antonio Castillo.
6 min readFeb 16, 2023

--

Photo by Tudor Baciu on Unsplash

Sometimes doing less is more, when it comes to optimizing performance of a react application a lot of the times this is the case, while using useMemo and useCallback is perfectly valid in a lot of cases, and they’re great tools in their own right, they could prove to be troublesome if not implemented correctly, this is why I’m going to list a few easy things to keep in mind when your app is starting to struggle.

  1. Stop using useEffect so much!

As React devs, we were taught early that useEffect could prove useful for simple side effects like updating a state variable when something changes, or maybe doing something with the props the component we’re working with receives, and while something like this might seem to work perfectly at first glance, it doesn’t come free of cost.

Effects run after the component has been rendered and all nodes have been committed to the DOM, which means that, for example; if you have an effect that checks prop values and consequently modifies the component’s state according to those values, React will do the following:

  1. Render the component and commit to the DOM.
  2. Run the effect, and modify the state.
  3. The state setter executed in the effect will then trigger a re-render of the component.
  4. Render and commit to the DOM again.

Do you notice anything strange about this behavior?

Well, the component is re-rendering unnecessarily!

To avoid this, the react team provides the following advice:

To avoid the unnecessary render passes, transform all the data at the top level of your components. That code will automatically re-run whenever your props or state change.

To make my point clearer, see this simple component:

const AddNumbers = ({ numArray }) => {
const [sum, setSum] = useState(0);

useEffect(() => {
if (numArray.length) {
const currentSum = numArray.reduce(
(accumulator, currentNum) => accumulator + currentNum
);
setSum(currentSum);
}
}, [numArray]);

return (
<>
<h1>Sum: {sum}</h1>
</>
);
};

The component takes an array as props, adds the values, and renders the result, quite simple, but it’s triggering an unnecessary re-render by running the effect.

Now see this code without the useEffect.

const AddNumbers = ({ numArray }) => {
const sum = (numArray.length ? numArray.reduce(
(accumulator, currentNum) => accumulator + currentNum
) : 0);

return (
<>
<h1>Sum: {sum}</h1>
</>
);
};

Not only did we remove the unnecessary re-render, but we also got rid of the need for state altogether and the component still updates when props change!

see this fragment from the new react docs:

When something can be calculated from the existing props or state, don’t put it in state. Instead, calculate it during rendering. This makes your code faster (you avoid the extra “cascading” updates), simpler (you remove some code), and less error-prone (you avoid bugs caused by different state variables getting out of sync with each other).

2. Pushing state down (Ironic huh?)

Sometimes, switching up our component structure might be enough to improve performance and minimize re-renders.

We all know the famous React pattern to lift state up when several children of the same parent need shared state, we also know the famous design pattern Container-presentational, where we keep the logic in a container component, and only use the children for the rendering and UI.

This is all well and good, but sometimes you have to get out of the traditional patterns and look at things from a different perspective.

Let’s say we have a Form component inside of a container with some other UI elements that might take some time to re-render because of their complexity. We have our event handlers for the form in the container and pass all of them through props to the form, keeping the logic out of it.

When we think about it, every time the form inputs change, every time the user types into it, or submits the form, it’s parent component will not only trigger the re-rendering of the form, but also re-render itself! Why is this?

Well, this is because the form’s state setters and handlers live inside the parent, and all state setters trigger re-renders, now imagine with every keystroke, everything close to it will re-render.

This could prove troublesome depending on the size and complexity of the rest of the components inside the parent.

We have this chunk of logic that only the form needs, so why not just push it down to the form, this would make that logic go into a lower node, and not affect any of it’s siblings, or it’s parent.

Here’s a quick example:

Notice the state and handlers live in the parent component, which is really common in React apps.

import "./styles.css";
import { useState } from "react";

const Sibling = () => {
return <div className="sibling">Sibling Component</div>;
};

const AddNumbers = ({ handler, submitHandler, num }) => {
return (
<>
<input onChange={handler} type="number" />
<button onClick={submitHandler}>Set number</button>
</>
);
};

export default function App() {
const [num, setNum] = useState(0);

const numberHandler = (e) => {
setNum(e.currentTarget.value);
};

const submitHandler = () => {
alert(`num is: ${num}`);
};

return (
<div className="App">
<AddNumbers
handler={numberHandler}
submitHandler={submitHandler}
num={num}
/>
<div className="container">
<Sibling />
<Sibling />
<Sibling />
<Sibling />
<Sibling />
</div>
</div>
);
}

If we open up the React-devtools profiler, and enable the component highlights for re-rendering we will see all the components inside the parent re-render unnecessarily, since we are setting a state variable with every keystroke, now, this is a small example to show how it works, but in a bigger application, this could trigger hundreds of re-renders.

Now, if we push the state down to the only component that needs it, which currently is only a presentational component, we can stop this unnecesary re-rendering, and encapsulate state.

See the following code after pushing state down.

const Sibling = () => {
return <div className="sibling">Sibling Component</div>;
};

const AddNumbers = () => {
const [num, setNum] = useState(0);

const numberHandler = (e) => {
setNum(e.currentTarget.value);
};

const submitHandler = () => {
alert(`num is: ${num}`);
};

return (
<>
<input onChange={numberHandler} type="number" />
<button onClick={submitHandler}>Set number</button>
</>
);
};

export default function App() {
return (
<div className="App">
<AddNumbers />
<div className="container">
<Sibling />
<Sibling />
<Sibling />
<Sibling />
<Sibling />
</div>
</div>
);
}

As you can see, the state was pushed down to the component that needs it, removing logic from the container, if we open up the performance profiler now, we no longer see the parent and siblings re-rendering when the user types in the input, or when the number state is modified, saving us tons of renders.

3. Use React.lazy and suspense.

Another tip could be to use React.lazy and Suspense to lazy-load components that aren’t immediately necessary for the initial view, such as those that are only needed for specific user interactions or those that aren’t required until later in the application flow.

import React, { lazy, Suspense } from "react";

// Lazy load the animation component
const Animation = lazy(() => import("./Animation"));

function App() {
return (
<div className="App">
<h1>Welcome to My App!</h1>
<Suspense fallback={<div>Loading...</div>}>
<Animation />
</Suspense>
</div>
);
}

export default App;

In this example, we’re using React.lazy to lazily load the Animation component when it's needed, instead of loading it with the rest of the app on the initial page load.

We’re also using Suspense to provide a fallback UI while the component is being loaded. In this case, we're simply rendering a loading message, but you could also render a spinner or other UI element to indicate that something is happening in the background.

Once the Animation component is loaded, it will be rendered just like any other component. This can help to improve the performance of your app by reducing the initial load time, especially if you have large or complex components that aren't needed right away.

Closing thoughts

As you can see, there’s some tweaks we can do to improve the performance of our app without the need for useMemo or useCallback, and simple things like these can go a long way for our application.

Finally, it’s always a good idea to use the latest version of React, as the React team is constantly working to improve performance and add new features that can help developers optimize their applications.

--

--