DEV Community

Cover image for React Performance Optimization
Bhdrpkcn
Bhdrpkcn

Posted on

React Performance Optimization

Introduction

Performance optimization in React is crucial for building fast, efficient, and responsive applications. React provides several built-in mechanisms and best practices to enhance performance, reducing unnecessary re-renders **and **improving load times. In this article, we will explore key concepts and strategies for optimizing React applications.

What Are React’s Key Concepts About Performance Optimization?

Virtual DOM (VDOM)
React uses a Virtual DOM (VDOM) to minimize direct manipulation of the real DOM, which is expensive in terms of performance. The VDOM is an abstraction of the real DOM, allowing React to make updates more efficiently.

vDOM

Diffing & Reconciliation
Diffing is the process React uses to compare the previous Virtual DOM with the new Virtual DOM to determine the minimum number of updates needed.

Reconciliation is React's algorithm that efficiently updates the real DOM by applying only the necessary changes, instead of re-rendering the entire component tree.

When Does a Re-render Happen? (The 4 Changing Rules)

React component re-renders when:

  1. State changes – When useState updates its value.
  2. Props change – When a parent component passes new props to a child.
  3. Context changes – When a useContext value updates.
  4. Force re-rendering – When useReducer or forceUpdate is triggered.

Before exploring possible optimizations, let’s first examine the hidden performance risk of unnecessary re-renders—an issue that comes from a very familiar place! 😊

github_re-render
Huge thanks to Aiden Bai

Let's dive in again, but this time, we'll explore a repository specifically created for this scenario—huge thanks to Theo Browne for making it!
Theo's Demo

In this demo, DemoComponent acts as the parent component, containing ColorPicker, CounterButton, and SlowComponent as its children. While updating the counter using CounterButton doesn’t noticeably impact performance, the situation changes significantly when interacting with ColorPicker, leading to noticeable slowdowns.
React renderer demo

The issue arises because a child component impacts the parent component's state. For example, ColorPicker relies on the color prop, and whenever it changes, it triggers a re-render of the entire DemoComponent, causing all child components to update unnecessarily.

function DemoComponent() {
  const [count, setCount] = useState(0);
  const [color, setColor] = useState("#ffffff");

  return (
    <div className={`flex gap-8`}>
      <div className="flex flex-col p-4 border border-white h-64 w-96 gap-4">
        <h2 className="text-xl font-bold mb-8 text-center">Color Picker</h2>
        <ColorPicker value={color} onChange={(e) => setColor(e)} />
        <div className="mt-2">
          Current value: <br />
          <span className="font-mono">{color}</span>
        </div>
      </div>
      <div className="flex flex-col p-4 border border-white h-64 w-96 gap-4">
        <h2 className="text-xl font-bold mb-8 text-center">Counter</h2>
        <CounterButton onClick={() => setCount((count) => count + 1)} />
        <div className="mt-2">
          Current value: <br />
          <span className="font-mono">{count}</span>
        </div>
      </div>
      <div className="flex flex-col p-4 border border-white h-64 w-96 gap-2">
        <h2 className="text-xl font-bold text-center">A Slow Component</h2>
        <span className="text-center text-neutral-200 font-light">
          (This component renders 10,000 boxes)
        </span>
        <SlowComponent unused={{ name: "nope" }} />
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

 So what Can We Do? What Can React Do?

To optimize performance, we can leverage React’s built-in tools and follow best practices:

Memoization Techniques

Memoization involves storing individual components to ensure they only re-render when their props change. Many people assume React does this automatically—meaning if props remain unchanged, React wouldn’t re-render. However, that’s not the case. By default, React always assumes something might have changed and re-renders just in case.

It’s up to us to explicitly tell React not to re-render using memoization techniques.

 Let's start with React.memo();

React.memo(): Wraps a functional component to prevent unnecessary re-renders when props remain the same.

import React, { useState } from "react";

function RealSlowComponent(props: { unused?: any }) {
  const largeArray = Array.from({ length: 10000 }, (_, i) => i);

  return (
    <div className="flex flex-wrap overflow-scroll gap-1">
      {largeArray.map((value) => (
        <div
          key={value}
          className="w-2 h-2 bg-neutral-700"
          style={{
            backgroundColor: `rgb(${value % 255}, ${(value * 2) % 255}, ${
              (value * 3) % 255
            })`,
          }}
        ></div>
      ))}
    </div>
  );
}

const SlowComponent = React.memo(RealSlowComponent);

function CounterButton(props: { onClick: () => void }) {
  return (
    <button
      onClick={props.onClick}
      className="px-4 py-2 bg-neutral-700 text-white rounded hover:bg-neutral-600 border border-white/20"
    >
      Increase count
    </button>
  );
}

Enter fullscreen mode Exit fullscreen mode

With that code above we attempted to stabilize SlowComponent by wrapping it with React.memo(RealSlowComponent), but it didn’t work as expected. 😕
SlowComponent still re-renders whenever ColorPicker is used! 😩🔄
React renderer demo

Why does this happen? 🤔

<SlowComponent unused={{ name: "nope" }} />

Enter fullscreen mode Exit fullscreen mode

If you take a closer look at the code, you’ll notice that SlowComponent receives a prop in DemoComponent. Even though this prop isn’t used anywhere inside SlowComponent, React still detects it as a potential change because, on every render, a new object is created and passed as a prop. Since object references change each time, React assumes the prop has been updated, causing unnecessary re-renders! 🚨

And that’s where useMemo() and useCallback() comes to the rescue—right when we need it the most! 🚀

useMemo(): Memoizes expensive computations so they are not re-calculated on every render, which is what we need in this case, letst wrap the 'unused" prop into useMemo() ;

useMemo(): **Memoizes function references so they do not change unnecessarily, preventing unnecessary re-renders of child components.

function memoizedCounterButton(props: { onClick: () => void }) {
  return (
    <button
      onClick={props.onClick}
      className="px-4 py-2 bg-neutral-700 text-white rounded hover:bg-neutral-600 border border-white/20"
    >
      Increase count
    </button>
  );
}

const CounterButton = React.memo(memoizedCounterButton);


function DemoComponent() {
//lets use useCallback function to prevent counterClick trigger unnecessary
  const handleCounterClick = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

//lets wrap out unused prop with useMemo
  const memoizedUnusedProp = useMemo(() => ({ name: "nope" }), []);

  return (
    <div>
      <div>
        <CounterButton onClick={handleCounterClick} />
      </div> 
      <div>
        <SlowComponent unused={memoizedUnusedProp} />
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

here is the current result :)

Optimized with useMemo and useCallback

Now we're getting to the core of this topic—why this blog was written in the first place! More importantly, we're exploring a simpler way to handle re-renders effectively.

React Compiler introduces an automatic optimization tool that takes care of memoization for you.

So, the choice is yours:

  • Manually optimize using React.memo, useMemo, and useCallback for fine-grained control.
  • Let the compiler handle it and focus on writing cleaner, more intuitive code.

Pick your approach based on your coding style and project needs! 🚀

WHAT ELSE WE CAN DO ?

  • Implement PureComponent
    A class component that only re-renders when its props or state change, automatically optimizing performance.

  • Use Key Prop in Lists
    Using a unique key in lists helps React efficiently track changes in list items and update only the necessary elements.

  • Avoid Inline Function Definitions in JSX
    Defining functions inline inside JSX creates a new function reference on every render, causing unnecessary re-renders. Instead, use useCallback() to stabilize function references.

  • Optimize ‘Expensive Renders’
    Use useMemo() and useCallback() memoize expensive calculations and prevent unnecessary re-computations.

  • List Virtualization
    Instead of rendering the entire list, render only the visible portion and dynamically load more items as the user scrolls. Libraries like react-window and react-virtualized can help achieve this.

  • Lazy Load Images
    Lazy loading defers the loading of off-screen images until they are needed, improving page load speed and performance.

  • Optimize Bundle Size

    1. Use tree shaking to remove unused code.
    2. Apply dead code elimination to minimize bundle size.
    3. Use code splitting to split the application into smaller chunks and prevent loading unnecessary files upfront.
  • Use Code Splitting
    Split code into smaller chunks to prevent loading unnecessary files upfront, reducing initial load time.

  • Leverage a CDN
    Using a Content Delivery Network (CDN) for assets, images, and scripts reduces latency and improves performance.

  • Image Optimization
    Compress images and use modern formats like WebP to improve loading speeds.

  • Reduce Overfetching
    Avoid fetching more data than necessary by implementing GraphQL or backend pagination strategies.

note to myself :

  • React doesn’t memoize components by default, so it re-renders more than expected.

  • Inline object and function definitions create new references on every render, leading to unnecessary re-renders.

  • React.memo() works best when props remain unchanged.

  • useMemo() prevents unnecessary re-renders by stabilizing prop references.

  • useCallback() ensures function references remain stable, reducing re-renders when functions are passed as props.

Beyond memoization, optimizing component structure, reducing redundant renders, and improving bundle size with techniques like list virtualization, lazy loading, and tree shaking can further enhance performance.

Final Thoughts

The Choice is Yours:

chooseMatrix

  • Manually optimize using React.memo, useMemo, and useCallback for full control.

  • Let the React Compiler handle it automatically, keeping your code cleaner and simpler.

By applying the right strategies, we can build efficient, high-performance React applications that deliver a smooth and responsive user experience! 🚀

please look :
React-Scan
and
Theo Browne's very useful video about this

Top comments (0)