Mehul Chaudhari

May 18, 2025 • 3 min read

Beyond React.memo: Strategic Performance Optimization in Complex React Applications

Beyond React.memo: Strategic Performance Optimization in Complex React Applications

Most React developers know about React.memo(), but treating it as a silver bullet for performance issues is a common mistake I see in production code. While React.memo() can be useful, it's just one tool in what should be a comprehensive optimization strategy.

The Limitations of React.memo

React.memo is a higher-order component that prevents unnecessary re-renders when props haven't changed:

const MemoizedComponent = React.memo(MyComponent);

Its limitations:

  • Performs shallow comparison of props by default

  • Doesn't help with components that use context

  • Won't prevent re-renders when a parent component re-renders

  • Adds overhead for components that rarely receive the same props

Strategic Optimization Techniques

1. Component Structure Matters

Often, the most effective optimization is restructuring components. By extracting the changing parts into separate components, we prevent expensive re-renders when unrelated state changes:

// Before: ExpensiveList re-renders when count changes
function ParentComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveList items={items} />
    </div>
  );
}

// After: Isolated state changes
function ParentComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <Counter count={count} setCount={setCount} />
      <ExpensiveList items={items} />
    </div>
  );
}

2. State Placement

Place state as close as possible to where it's used. When state is placed high in the component tree, all child components receive new props on state updates, triggering unnecessary re-renders:

// Instead of this at app level
function App() {
  const [userSettings, setUserSettings] = useState({});
  // Many nested components later...
  return <DeepNestedTree userSettings={userSettings} />;
}

// Place state closer to usage
function UserSettingsSection() {
  const [userSettings, setUserSettings] = useState({});
  return <SettingsPanel settings={userSettings} />;
}

3. Effective Use of useCallback

When passing functions as props, avoid recreating them on every render. Functions are objects in JavaScript, and creating new function instances causes child components to see "different" props, triggering unnecessary re-renders:

// Problem - new function created on every render
function MyComponent({ onSubmit }) {
  const [formData, setFormData] = useState({});
  
  const handleSubmit = () => {
    onSubmit(formData);
  };
  
  return <ExpensiveChild onSubmit={handleSubmit} />;
}

// Solution - memoize the callback
function MyComponent({ onSubmit }) {
  const [formData, setFormData] = useState({});
  
  const handleSubmit = useCallback(() => {
    onSubmit(formData);
  }, [onSubmit, formData]);
  
  return <ExpensiveChild onSubmit={handleSubmit} />;
}

4. Split Context by Update Frequency

Context triggers re-renders for all consumers when its value changes. By splitting contexts based on how frequently their values change, you reduce the number of components that re-render when any part of your application state updates:

// Instead of one large context
const AppContext = createContext({
  user: {},         // Changes rarely
  theme: {},        // Changes occasionally
  notifications: [] // Changes frequently
});

// Split by update frequency
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

5. Windowing for Long Lists

For long lists, implement virtualization to render only visible items. This prevents rendering thousands of DOM elements at once, which blocks the main thread and consumes excessive memory:

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ListItem data={items[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={500}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

When React.memo Makes Sense

After applying structural optimizations, selectively use React.memo for:

  • Pure components that render frequently

  • Components that receive the same props often

  • Expensive components deep in the tree

Use custom comparison for complex props:

const MemoizedComponent = React.memo(MyComponent, (prev, next) => {
  return prev.item.id === next.item.id;
});

Conclusion

Effective React optimization is about strategy, not just applying memo everywhere. Start with component structure and state placement, then measure and apply targeted optimization where it matters most. These are my observations; I’d love to hear more ways, views, or techniques you’ve found effective!


What performance optimization techniques have you found most effective in your React applications?

Join Mehul on Peerlist!

Join amazing folks like Mehul and thousands of other people in tech.

Create Profile

Join with Mehul’s personal invite link.

2

21

2