React Performance: Two Patterns to Try Before Reaching for memo()

React developers facing slow state updates usually reach for the same checklist: verify a production build, confirm the state isn’t higher than necessary, then profile and wrap expensive subtrees in memo() or useMemo(). That last step is tedious, especially for intermediate components, and ideally a compiler would handle it automatically someday.

Before going down that optimization path, there are two surprisingly basic techniques worth trying first. They complement — not replace — memo() and useMemo(), but often make those measures unnecessary.

The Setup: An Expensive Subtree

Consider a component with severe rendering performance issues:

import { useState } from 'react';
 
export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}
 
function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}

When color updates inside App, the entire tree re-renders, including the artificially slow <ExpensiveTree />. While memo() offers a direct fix, two alternative solutions avoid it entirely.

Solution 1: Extract State into a Child Component

Examining the render code reveals that only a portion of the returned tree depends on color:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

The fix is to extract the color-dependent parts into a new Form component and move the state down into it:

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  );
}
 
function Form() {
  let [color, setColor] = useState('red');
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  );
}

Now, when color changes, only Form re-renders. The expensive subtree is untouched. This pattern has the side benefit of making data flow easier to trace, since you’re not plumbing state through components that don’t need it.

Solution 2: Lift JSX Content as Children

The first solution fails when the state must live above the expensive component. Suppose color is used by the parent <div> wrapping <ExpensiveTree />:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

It seems impossible to extract the non-color parts into their own component, since the parent <div> depends on color and would carry the expensive tree along with it. But there’s a remarkably simple answer:

export default function App() {
  return (
    <ColorPicker>
      <p>Hello, world!</p>
      <ExpensiveTree />
    </ColorPicker>
  );
}
 
function ColorPicker({ children }) {
  let [color, setColor] = useState("red");
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
}

Split App into two components. The state variable and components that depend on color move into ColorPicker. The parts that don't use color remain in App and are passed to ColorPicker as JSX content — specifically, the children prop.

When color updates, ColorPicker re-renders, but since it receives the same children prop from its parent, React skips that subtree entirely. <ExpensiveTree /> never re-renders.

A Note on Future Benefits

Passing content via the children prop also opens doors beyond client-side rendering. With React Server Components, ColorPicker could receive its children from the server, potentially running <ExpensiveTree /> (either whole or in part) on the server. A top-level state update wouldn't re-render those server-sourced parts on the client — something even memo() cannot achieve.

Both techniques serve a broader goal than raw performance: splitting stateful and stateless parts of the tree results in cleaner component boundaries and reduces prop drilling. Whether these patterns alone are sufficient depends on your component structure, but they’re good first attempts.