DEV Community

Cover image for Why Does Your React App Re-render So Much?
Tanu Priya
Tanu Priya

Posted on

Why Does Your React App Re-render So Much?

You update one small piece of state:

setCount(count + 1);
Enter fullscreen mode Exit fullscreen mode

And suddenly your console looks like this:

Header rendered
Sidebar rendered
ProductList rendered
ProductCard rendered
Footer rendered
Enter fullscreen mode Exit fullscreen mode

Your first thought might be:

"Why did half of my React app render again? Isn't this bad for performance?"

Not necessarily.

One of the biggest misconceptions about React performance is that every re-render is a problem.

It isn't.

React components are designed to render when their inputs change. A render is simply part of how React determines what the UI should look like.

The real question is not:

"How do I stop React from re-rendering?"

It's:

"Is this re-render causing unnecessary expensive work?"

That distinction is the foundation of understanding React performance.


What Does a Re-render Actually Mean?

Consider this simple component:

function Counter() {
  const [count, setCount] = useState(0);

  console.log("Counter rendered");

  return (
    <>
      <h1>{count}</h1>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

When the button is clicked, count changes.

React then needs to calculate what the component should produce with the new state.

A simplified mental model is:

State changes
     ↓
Component renders
     ↓
Next UI is calculated
     ↓
React reconciles the result
     ↓
Necessary changes are committed
Enter fullscreen mode Exit fullscreen mode

The important distinction is:

Re-render ≠ Complete DOM rebuild
Enter fullscreen mode Exit fullscreen mode

A re-render means React performs rendering work again to determine the component's next output.

It does not mean React destroys the entire browser DOM and recreates it from scratch.

In fact, React can render a component and discover that little—or even nothing—needs to change in the actual DOM.

So:

A re-render is normal. Unnecessary expensive work is what you should care about.


What Causes a Component to Render?

Before optimizing a component, you need to understand why it rendered in the first place.

There are several common causes.

1. Its State Changes

This is the most obvious case.

const [count, setCount] = useState(0);
Enter fullscreen mode Exit fullscreen mode

When you call:

setCount(count + 1);
Enter fullscreen mode Exit fullscreen mode

React receives a state update and schedules work to update the component.

The component needs to calculate its UI using the new state.

That's completely normal.


2. Its Parent Renders

This is where many developers get confused.

Consider:

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>

      <Header />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now imagine:

function Header() {
  console.log("Header rendered");

  return <h1>My Website</h1>;
}
Enter fullscreen mode Exit fullscreen mode

When count changes, App renders again.

As part of processing the updated tree, React may also render Header.

So you might see:

Header rendered
Enter fullscreen mode Exit fullscreen mode

even though Header doesn't use count.

This often leads developers to think:

"React is unnecessarily rendering my entire application!"

But that's too simplistic.

If Header is tiny and its render is cheap, another render may have practically no meaningful performance impact.

The important question is not whether Header rendered.

It's:

How much work did that render actually do?


3. Its Props Change

Props are another source of updates.

function Profile({ name }) {
  return <h1>{name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Used like this:

function App() {
  const [name, setName] = useState("Nayan");

  return <Profile name={name} />;
}
Enter fullscreen mode Exit fullscreen mode

When name changes, Profile receives different input and needs to calculate its new UI.

That's exactly what we expect.

New props
   ↓
New component input
   ↓
New render
Enter fullscreen mode Exit fullscreen mode

Again, this isn't automatically a performance problem.


4. A Context Value Changes

Context can also cause components that consume that context to update.

For example:

const ThemeContext = createContext();
Enter fullscreen mode Exit fullscreen mode
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

A component can consume it:

function Navbar() {
  const theme = useContext(ThemeContext);

  return <nav className={theme}>Navbar</nav>;
}
Enter fullscreen mode Exit fullscreen mode

When the context value changes, components consuming that context can update.

This becomes especially important when one context contains many unrelated pieces of frequently changing state.

For example:

User
Theme
Notifications
Cart
Language
Enter fullscreen mode Exit fullscreen mode

If everything lives inside one large context, a change to one part can cause more consumers to be affected than necessary.

Sometimes separating unrelated concerns into smaller contexts is a better design.


5. State Used by a Custom Hook Changes

Custom hooks don't render independently.

Consider:

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  return width;
}
Enter fullscreen mode Exit fullscreen mode

And:

function App() {
  const width = useWindowWidth();

  return <h1>{width}px</h1>;
}
Enter fullscreen mode Exit fullscreen mode

If the hook's state changes, the component using that hook is updated.

A useful rule is:

Hooks don't render independently. The component using them renders.

This distinction becomes helpful when debugging applications with many custom hooks.


The Biggest Source of Unnecessary Work: State in the Wrong Place

Now we get to one of the most useful React performance ideas.

Consider this:

function App() {
  const [search, setSearch] = useState("");

  return (
    <>
      <SearchBox
        search={search}
        setSearch={setSearch}
      />

      <ExpensiveDashboard />
      <Sidebar />
      <Footer />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Imagine the user types:

N
Na
Nay
Naya
Enter fullscreen mode Exit fullscreen mode

Every keystroke changes search.

Because search lives in App, App updates on every keystroke.

That means React has to process the tree below that component repeatedly.

Now ask:

Does search actually need to live in App?

If only SearchBox needs that state, placing it closer to SearchBox can reduce the scope of the updates.

For example:

App
├── SearchBox
│     └── Search state
├── ExpensiveDashboard
├── Sidebar
└── Footer
Enter fullscreen mode Exit fullscreen mode

Instead of:

App
├── Search state
├── SearchBox
├── ExpensiveDashboard
├── Sidebar
└── Footer
Enter fullscreen mode Exit fullscreen mode

This idea is called state colocation.

And it is one of the most useful optimization techniques in React because you're improving the structure of the application rather than adding optimization APIs everywhere.

Before reaching for:

React.memo(...)
Enter fullscreen mode Exit fullscreen mode

or:

useMemo(...)
Enter fullscreen mode Exit fullscreen mode

or:

useCallback(...)
Enter fullscreen mode Exit fullscreen mode

ask:

Can this state live closer to where it is actually needed?

Often, that's the better solution.


Why "Re-render" Doesn't Mean "DOM Update"

This distinction is worth making extremely clear.

Suppose React renders:

<h1>Welcome</h1>
Enter fullscreen mode Exit fullscreen mode

and the next render produces:

<h1>Welcome</h1>
Enter fullscreen mode Exit fullscreen mode

The component rendered.

But the resulting UI is effectively the same.

So React may have no meaningful DOM change to commit.

That's why these concepts should stay separate:

Render
   ↓
Reconciliation
   ↓
Commit
   ↓
DOM changes
Enter fullscreen mode Exit fullscreen mode

A render can happen without a meaningful DOM update.

This is why seeing:

Component rendered
Enter fullscreen mode Exit fullscreen mode

in the console is not enough evidence that something is wrong.


What Is Reconciliation?

During an update, React needs to determine how the new element tree relates to the previous one.

Imagine:

Previous UI

Count: 0
Enter fullscreen mode Exit fullscreen mode

becoming:

Next UI

Count: 1
Enter fullscreen mode Exit fullscreen mode

The surrounding structure may remain the same while the displayed value changes.

React's reconciliation process determines how the current tree should transition toward the next one.

You may have heard this explained as:

"React compares the old Virtual DOM with the new Virtual DOM."

That's a useful beginner-friendly explanation, but it's an oversimplification.

React has its own internal representations and reconciliation machinery. It isn't simply taking two snapshots of the browser DOM and comparing them.

For practical React development, the more useful mental model is:

React calculates the next UI and determines what work is required to transition the current tree toward it.


What Happens During the Commit?

After React finishes the necessary rendering and reconciliation work, it reaches the commit phase.

This is where React applies the required changes to the host environment.

In a browser, that means updating the DOM.

For our counter:

Count: 0
Enter fullscreen mode Exit fullscreen mode

eventually becomes:

Count: 1
Enter fullscreen mode Exit fullscreen mode

The key point is that React doesn't need to rebuild the entire DOM simply because a component rendered again.

It commits the changes that are actually required.


What About the Browser?

React isn't the final step.

After the DOM has been updated, the browser still needs to turn that state into pixels.

A simplified browser pipeline looks like:

DOM + CSS
   ↓
Style
   ↓
Layout
   ↓
Paint
   ↓
Composite
   ↓
Pixels
Enter fullscreen mode Exit fullscreen mode

This is important because React performance and browser rendering performance aren't exactly the same thing.

You can have:

  • expensive React rendering
  • expensive DOM work
  • expensive layout
  • expensive painting

Understanding where the actual cost occurs is much more useful than simply counting renders.


Why Can Re-renders Be Expensive?

A re-render itself isn't necessarily expensive.

The work performed during that render might be.

Imagine a component that:

  • filters thousands of records
  • renders a huge list
  • performs complex calculations
  • renders a complicated chart
  • causes many expensive children to update

Now frequent renders may matter.

Compare:

Render
  ↓
Small component
  ↓
Cheap work
  ↓
Probably fine
Enter fullscreen mode Exit fullscreen mode

with:

Render
  ↓
Heavy calculations
  ↓
Large list
  ↓
Expensive children
  ↓
Noticeable lag
Enter fullscreen mode Exit fullscreen mode

That's the difference.

So don't optimize because you see the word:

rendered
Enter fullscreen mode Exit fullscreen mode

in your console.

Optimize when there is unnecessary work that actually matters.


What About React.memo?

React.memo can prevent a child component from rendering when its props haven't changed, assuming the relevant comparison indicates that the props are equal.

For example:

const Profile = React.memo(function Profile({ name }) {
  console.log("Profile rendered");

  return <h1>{name}</h1>;
});
Enter fullscreen mode Exit fullscreen mode

Now:

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>

      <Profile name="Nayan" />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

When count changes, App renders again.

But Profile still receives the same name.

Because it's memoized, React can potentially skip rendering Profile.

Conceptually:

Parent renders
      ↓
Child props unchanged
      ↓
Memoization can skip child render
Enter fullscreen mode Exit fullscreen mode

But React.memo isn't something you should automatically add to every component.

It is most useful when:

  • the parent renders frequently
  • the child is relatively expensive
  • the child's props often remain unchanged
  • avoiding the render actually provides a measurable benefit

For a tiny component like:

function Label({ text }) {
  return <span>{text}</span>;
}
Enter fullscreen mode Exit fullscreen mode

memoization may provide little practical benefit.

Don't use React.memo because a component can re-render. Use it when skipping that render is actually useful.


The Hidden Problem: New Object References

There is another reason memoization can sometimes fail to provide the expected benefit.

Consider:

const Profile = React.memo(function Profile({ user }) {
  return <h1>{user.name}</h1>;
});
Enter fullscreen mode Exit fullscreen mode

And:

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>

      <Profile user={{ name: "Nayan" }} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Every time App renders, this creates a new object:

{ name: "Nayan" }
Enter fullscreen mode Exit fullscreen mode

Even though the contents are the same, the object itself is a new reference.

Conceptually:

Previous render → Object A
Next render     → Object B

Object A !== Object B
Enter fullscreen mode Exit fullscreen mode

This matters because memoization compares props using shallow equality.

The same idea can apply to arrays and functions.

For example:

<Profile items={["React", "Next.js"]} />
Enter fullscreen mode Exit fullscreen mode

or:

function App() {
  const handleClick = () => {
    console.log("Clicked");
  };

  return <Child onClick={handleClick} />;
}
Enter fullscreen mode Exit fullscreen mode

A new function is created during each render.

But don't take this to mean:

"New objects and functions are bad."

They're not.

Creating a new object or function is perfectly normal JavaScript.

It only becomes relevant when reference stability is important for a specific optimization.


So What Is useCallback For?

useCallback can preserve a function reference between renders when its dependencies haven't changed.

For example:

const handleClick = useCallback(() => {
  console.log("Clicked");
}, []);
Enter fullscreen mode Exit fullscreen mode

This can be useful when the callback is passed to a memoized child:

const Child = React.memo(function Child({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
});
Enter fullscreen mode Exit fullscreen mode

Without stable references, a newly created callback can cause the child's props to appear changed.

But that doesn't mean every function needs useCallback.

This:

const handleClick = () => {
  console.log("Clicked");
};
Enter fullscreen mode Exit fullscreen mode

is perfectly fine in many situations.

The better rule is:

Use useCallback when a stable function reference enables a meaningful optimization.

Not simply because a function is created during rendering.


And What About useMemo?

useMemo can help avoid repeating an expensive calculation when its dependencies haven't changed.

For example:

const filteredProducts = useMemo(() => {
  return products.filter((product) => product.inStock);
}, [products]);
Enter fullscreen mode Exit fullscreen mode

This can make sense when the calculation is genuinely expensive.

It can also be useful when you need a stable value reference for another optimization.

But this:

const name = useMemo(() => "Nayan", []);
Enter fullscreen mode Exit fullscreen mode

usually adds complexity without providing a meaningful benefit.

So again:

useMemo is an optimization tool, not a default requirement.


The Most Common React Performance Mistake

A developer opens the console and sees:

Dashboard rendered
Dashboard rendered
Dashboard rendered
Dashboard rendered
Enter fullscreen mode Exit fullscreen mode

They immediately add:

React.memo
useMemo
useCallback
Enter fullscreen mode Exit fullscreen mode

everywhere.

But they haven't answered the most important question:

Is the render actually causing a performance problem?

A small component can render many times and still be completely fine.

Meanwhile, a large component doing expensive work may be worth optimizing even if it renders less frequently.

So don't optimize based on the number of renders alone.

Optimize based on the cost of the work.


How to Investigate a Re-render

When you notice frequent rendering, work through these questions.

1. What caused it?

Was it:

  • state?
  • props?
  • context?
  • a parent update?
  • state used by a custom hook?
  • another update source?

First understand the trigger.


2. Does the component actually need that state?

If frequently changing state is stored high in the tree, ask whether it can move closer to the components that actually use it.

State colocation is often a cleaner solution than memoization.


3. Is the render expensive?

Look for:

  • large lists
  • heavy calculations
  • complex charts
  • expensive child components
  • slow interactions
  • visible UI lag

If the render is cheap, there may be nothing to fix.


4. Can the component structure be improved?

Sometimes the best optimization isn't an optimization API.

It might simply be:

Better component boundaries
Better state placement
Smaller update scope
Enter fullscreen mode Exit fullscreen mode

Good architecture can prevent unnecessary work naturally.


5. Would memoization actually help?

Only after understanding the problem should you consider:

React.memo
useMemo
useCallback
Enter fullscreen mode Exit fullscreen mode

And ideally, measure whether the change actually improved the situation.


The React Performance Mindset

This is the mindset I want you to remember.

When you see:

Component rendered
Enter fullscreen mode Exit fullscreen mode

don't immediately think:

"Something is wrong."

Instead ask:

Why did it render?

Then:

What work did the render perform?

Then:

Is that work expensive enough to matter?

And finally:

What's the simplest way to reduce that work?

That might be:

Move state
     ↓
Split components
     ↓
Reduce unnecessary work
     ↓
Memoize when useful
Enter fullscreen mode Exit fullscreen mode

Not:

Something rendered
     ↓
useMemo
     ↓
useCallback
     ↓
React.memo
     ↓
Everything is memoized
Enter fullscreen mode Exit fullscreen mode

The Mental Model to Remember

React performance becomes much easier to understand when you separate these concepts:

State / Props / Context change
            ↓
      React renders
            ↓
   Next UI is calculated
            ↓
      Reconciliation
            ↓
          Commit
            ↓
      DOM is updated
            ↓
    Browser renders pixels
Enter fullscreen mode Exit fullscreen mode

But don't confuse:

Re-render
    ≠
DOM update
Enter fullscreen mode Exit fullscreen mode

or:

Parent re-render
    ≠
Performance problem
Enter fullscreen mode Exit fullscreen mode

or:

Memoization
    ≠
Always necessary
Enter fullscreen mode Exit fullscreen mode

Final Takeaway

React components are supposed to render.

A state change, a parent update, changed props, or a context update can cause rendering work.

That alone isn't a problem.

The real problem is unnecessary expensive work.

So when you see a component rendering more often than expected, don't immediately reach for React.memo, useMemo, or useCallback.

Instead:

Notice the render
      ↓
Understand what triggered it
      ↓
Measure the actual cost
      ↓
Improve component boundaries
      ↓
Keep state close to where it's needed
      ↓
Reduce unnecessary work
      ↓
Use memoization when it provides real value
Enter fullscreen mode Exit fullscreen mode

The goal isn't to build an application where nothing re-renders.

The goal is to build an application where the work that happens during updates is appropriate for what actually changed.

So don't ask:

"How do I stop React from re-rendering?"

Ask:

"Is this render doing unnecessary work, and what's the simplest way to fix it?"

That small change in mindset is one of the most important steps toward understanding React performance.

And once you understand why a component rendered, what work it performed, and what actually reached the DOM, React optimization stops being a collection of tricks and starts becoming a reasoning problem.

Top comments (0)