DEV Community

Cover image for Best practices for high-performance React applications
Rolf Ripszam
Rolf Ripszam

Posted on

1 1 1

Best practices for high-performance React applications

-- 10 min read --

This post describes some best practices to avoid the most common performance bottlenecks that occur in React applications.


First of all: Component updates and referential equality

Understanding why a React component updates (re-renders) is crucial for performance optimization, as unnecessary updates are one of the most common sources of poor performance.

By default, whether a component update is necessary is decided using a referential equality check (see explanations below).

Primitives (e.g. string, number, boolean, undefined, null) get compared by value 1:

const str1 = "A";
const str2 = "A";
const str3 = "B";
console.log(str1 === str1); // true
console.log(str1 === str2); // true
console.log(str1 === str3); // false
Enter fullscreen mode Exit fullscreen mode

Objects, arrays and functions by reference 1:

const obj1 = { a: "A", b: "B" };
const obj2 = { a: "A", b: "B" };
const obj3 = { a: "A", c: "C" };
console.log(obj1 === obj1); // true
console.log(obj1 === obj2); // false
console.log(obj1 === obj3); // false
Enter fullscreen mode Exit fullscreen mode

Memoization of props

One reason for a component update is a change in props (comparing two consecutive updates).

By default, each prop is compared using a referential equality check (see above).

Therefore, you should, in most cases, avoid passing unmemoized references as props.

If you pass a new reference (inline or otherwise), as shown below, ChildComponent will update on every parent update.

Don't do this

// objProp is a new reference when this code runs
const objProp = { someKey: 'someValue' };
// funcProp is a new reference when this code runs
const funcProp = () => ({ someKey: 'someValue' });
// arrProp is a new reference when this code runs
return <ChildComponent objProp={objProp} arrProp={[]} funcProp={funcProp} />;
Enter fullscreen mode Exit fullscreen mode

Instead, memoize your props

const objProp = React.useMemo(() => ({ someKey: 'someValue' }), []);
const arrProp = React.useMemo(() => [], []);
const funcProp = React.useCallback(() => ({ someKey: 'someValue' }), []);
return <ChildComponent objProp={objProp} arrProp={arrProp} funcProp={funcProp} />;
Enter fullscreen mode Exit fullscreen mode

Likewise, avoid passing props that the component doesn’t use (e.g., by spreading objects). This can also lead to wasteful updates since React does not know which of the props are used and which are not.

Don't do this

<ChildComponent {...someUnnecessaryProps} />;
Enter fullscreen mode Exit fullscreen mode

Selective subscription to the Redux state

Another reason for a component update is a subscription to the Redux state.

The useSelector hook runs the provided selector function on every Redux state change and triggers a component update if the returned value has changed (referentially, see above).

Don't do this

// component containing this useSelector hook updates every time the returned reference changes
const todoState = useSelector(selectTodoState);
const value: string = todoState.todos.find(t => t.id === idToFind).someValue;
Enter fullscreen mode Exit fullscreen mode

Instead, select only the minimal piece of state your component needs, to increase the chances that the selected value remains referentially equal between updates.

Do this instead

// component containing this useSelector hook updates only when the returned string value changes
const value = useSelector((state) => {
  const todoState = selectTodoState(state);
  const value: string = todoState.todos.find(t => t.id === idToFind).someValue;
  return value;
});
Enter fullscreen mode Exit fullscreen mode

If your selector must return a new reference (e.g., an array or object) on each call, consider using 'reselect' to memoize the result.

Don't do this

const selectTransformedTodos = (state, todoType) => {
  const todos = createTodoSelector(todoType)(state);
  // returns a new array reference on every call of the selector
  return todos.map(todoTransformer); 
}
Enter fullscreen mode Exit fullscreen mode

Use a memoized selector

import { createSelector } from "reselect";
// call of createSelector creates a new memoized selector instance
const selectTransformedTodos = createSelector(
// 'input selectors' => get called on every Redux state change
[(state, todoType) => createTodoSelector(todoType)(state)], 
// 'output selector' => only gets called if the returned value 
// by at least one 'input selector' changes (referentially)
// compared to the previous run
(todos) => { 
  // returns a cached stable value if the results of the 'input selectors' 
  // stay referentially equal
  return todos.map(todoTransformer);
});
Enter fullscreen mode Exit fullscreen mode

Memoize components

When a component updates, React will by default also update all of its children — unless you prevent it explicitly.

If a component only updates because its parent did (can be seen when profiling, see below), you can prevent this by wrapping it in React.memo.

Important!
Don't wrap all your components with React.memo, as it also has a certain performance cost.


Position of hooks matters

Hooks such as useState, useReducer, useContext, and useEffect can all trigger component updates.

The position of these hooks in the component tree matters, like in the example below:

Clicking the toggle button will update the entire View component, including Contentbox, Footer, and BigExpensiveComponent — even though only part of the UI actually changes.

Don't do this

const View = () => {
  const [showElements, setShowElements] = React.useState(false);
  const toggleElements = React.useCallback(
    () => setShowElements((previousValue) => !previousValue),
    [],
  );
  return (
    <Contentbox
      heading={
        <>
          <Button onClick={toggleElements} />
          {showElements && <AdditionalElements />}
        </>
      }
      footer={<Footer />}
    >
      <BigExpensiveComponent />
    </Contentbox>
  );
};
Enter fullscreen mode Exit fullscreen mode

A better approach is to extract a separate Heading component, since the local state is only relevant there.

Do this instead

const Heading = () => {
  const [showElements, setShowElements] = React.useState(false);
  const toggleElements = React.useCallback(
    () => setShowElements((previousValue) => !previousValue),
    [],
  );
  return (
    <>
      <Button onClick={toggleElements} />
      {showElements && <AdditionalElements />}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Addition: Instructions for profiling

  1. Install the React Developer Tools in your browser
  2. In the settings, check the flag to record why each component rendered while profiling
  3. Start the profiler
  4. Perform a certain action in your app that has poor performance
  5. Stop the profiler
  6. Inspect the recorded frames and analyze the involved component updates
  7. If you think that an update is unnecessary, try to prevent it in your code
  8. Repeat steps 3–7 until you've eliminated all unnecessary updates

Bottom Line

The goal of this content is to make you aware of how React works and how implementation affects performance.

Writing an unperformant app happens easily, as small details can have significant impacts.

Preventing a few unnecessary update cycles might not drastically change your application's performance immediately.

However, consistently applying these best practices can cumulatively have a huge impact, transforming an unusable app into a highly efficient one.


Thank you for reading the article ❤️!

Feel free to share your feedback in the comments.


  1. React internally uses Object.is(), which behaves identical to === for most practical purposes. For simplicity, this post refers to ===. 

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (1)

Collapse
 
dotallio profile image
Dotallio

Memoizing props changes everything.

DevCycle image

Ship Faster, Stay Flexible.

DevCycle is the first feature flag platform with OpenFeature built-in to every open source SDK, designed to help developers ship faster while avoiding vendor-lock in.

Start shipping

👋 Kindness is contagious

Sign in to DEV to enjoy its full potential—unlock a customized interface with dark mode, personal reading preferences, and more.

Okay