How to Optimize Class Components in React

While modern React development heavily favors functional components and hooks, many legacy applications still rely on class components. Optimizing these components is crucial for maintaining application performance, reducing unnecessary re-renders, and ensuring a smooth user experience. This article covers key strategies to optimize React class components, including implementing lifecycle methods, using pure components, avoiding inline bindings, and managing prop structures.

1. Implement shouldComponentUpdate

By default, a React class component re-renders whenever its parent component re-renders or when its own state or props change. You can prevent unnecessary re-renders by implementing the shouldComponentUpdate lifecycle method. This method receives the next props and state, allowing you to return false if the changes do not affect the UI.

shouldComponentUpdate(nextProps, nextState) {
  // Only re-render if the ID or name changes
  return nextProps.id !== this.props.id || nextState.name !== this.state.name;
}

2. Extend React.PureComponent

If you do not want to write manual comparison logic in shouldComponentUpdate, you can extend React.PureComponent instead of React.Component. A PureComponent automatically performs a shallow comparison of props and state to determine if a re-render is necessary.

import React, { PureComponent } from 'react';

class UserProfile extends PureComponent {
  render() {
    return <div>{this.props.username}</div>;
  }
}

Note: Because PureComponent performs a shallow comparison, it may not detect changes inside deeply nested objects or arrays.

3. Bind Functions in the Constructor or Use Arrow Functions

Binding event handlers inside the render method or using inline arrow functions causes React to create a new function instance on every single render. This triggers unnecessary re-renders of child components that receive these functions as props.

Inefficient (creates new function on every render):

render() {
  return <button onClick={() => this.handleClick()}>Click Me</button>;
}

Efficient (bound once in the constructor):

constructor(props) {
  super(props);
  this.handleClick = this.handleClick.bind(this);
}

Efficient (using class fields syntax):

handleClick = () => {
  console.log(this.props.message);
};

4. Avoid Passing Inline Objects and Arrays as Props

Passing inline objects or arrays directly to child components creates a new reference on every render. Because the reference changes, child components (even those extending PureComponent) will re-render unnecessarily.

Inefficient:

render() {
  return <ChildComponent options={['admin', 'user']} />;
}

Efficient:

// Define static arrays/objects outside of the class
const USER_ROLES = ['admin', 'user'];

class Dashboard extends React.Component {
  render() {
    return <ChildComponent options={USER_ROLES} />;
  }
}

5. Use Code Splitting with React.lazy

Large class components can bloat your application’s bundle size. You can optimize load times by splitting your components into smaller chunks and loading them only when needed using React.lazy and React.Suspense.

import React, { Component, Suspense, lazy } from 'react';

const HeavyClassComponent = lazy(() => import('./HeavyClassComponent'));

class App extends Component {
  render() {
    return (
      <Suspense fallback={<div>Loading...</div>}>
        <HeavyClassComponent />
      </Suspense>
    );
  }
}