Skip to main content

Performance

Premature Optimization

Premature optimization is the root of all evil -- DonaldKnuth

Premature Optimization is optimizing before we know that we need to do it.

Recommendation: Get your application working and then near the end of a development cycle take the time to optimize for performance.

What causes a component to render in React?

A re-render can only be triggered if a component’s state has changed. The state can change from a props change, or from a call to setState or a useState update state function. The component gets the updated state and React decides if it should re-render the component. Unfortunately, by default React is incredibly simplistic and basically re-renders everything all the time.

Component changed? Re-render. Parent changed? Re-render parent and all it's children. Section of props that doesn't actually impact the view changed? Re-render.

Summary

  • Default Behavior: Changing state

    • results in that component and all descendants being re-rendered.
  • Default Behavior: Update a prop in a component

    • results in that component and all descendants re-rendered.
    • the check for whether a prop changed uses a strict equality check ===
    const a = { "test": 1 };
    const b = { "test": 1'};

    console.log(a === b); // will be false

    const c = a; // "c" is just a reference to "a"

    console.log(a === c); // will be true

Wasted Renders

React has two phrases that run sequentially to update the UI.

  1. Render Phase

    The "render phase" is where React compares a previous version of a Virtual DOM representing the UI with an updated version to figure out what if any changes need to be made.

  2. Commit Phase

    The "commit phase" is where React actually changes the real DOM.

As demonstrated in the Virtual DOM chapter React is very efficient about figuring out the minimal DOM operations to make in the "render phase" and batches them to make rendering the UI extremely performant.

However, the "render phase" does take work and consumes resources and should not take place if it isn't needed. If all the components on the screen are constantly rendering when the don't need to this is a common source of eventual performance problems. We call this problem: "wasted renders".

Wasted Renders can be fixed using:

  • React.Memo when using function components.
  • React.PureComponent when using class components.

React.memo

const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});

React.memo is a higher order component for function components and subsequently can only be used with function components.

If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

React.memo only checks for prop changes. If your function component wrapped in React.memo has a useState or useContext Hook in its implementation, it will still rerender when state or context change.

By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.

function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);

This method only exists as a performance optimization. Do not rely on it to "prevent" a render, as this can lead to bugs.

React.PureComponent

React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn't implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.

If your React component's render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.

Note

React.PureComponent's shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.

Furthermore, React.PureComponent's shouldComponentUpdate() skips prop updates for the whole component subtree. Make sure all the children components are also "pure".


Note

Unlike the shouldComponentUpdate() method on class components, the areEqual function returns true if the props are equal and false if the props are not equal. This is the inverse from shouldComponentUpdate.

FAQs

What is memoization?

In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

Why is my component rendering twice?

Remove the <React.StrictMode> tag as shown below and this behavior will go away however you may not want to remove it as it doesn't happen in production. For more information, see the Strict Mode Documentation or this stackoverflow question: Strict Mode Rendering Twice.

index.js

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
- <React.StrictMode>
{app}
- </React.StrictMode>
,
document.getElementById('root')
);

React.memo Demo

Run the demo below and open the console to observe some wasted renders.

Steps:

  1. Before beginning the demos in this chapter add the following css class if it doesn't already exist.

    styles.css

    .box {
    border: 1px dashed;
    padding: 30px;
    }
  2. Paste the code below into main.js

  3. Open the application in a browser.

  4. Open Chrome DevTools and switch to the console.

  5. Type in the add textbox to add an item and then click the add button.

  6. Notice that every item in the list re-renders even though you only added one item.

    Note: Updating or removing an item also causes everything to re-render.

  7. Commment out the ListItem component.

  8. Uncomment the ListItem component below the original wrapped in a React.memo function.

  9. Refresh your browser.

  10. Once again type in the add textbox to add an item and then click the add button.

  11. Notice that only one item in the list re-renders since the other ListItem's are the same. You have successfully eliminated some wasted renders.

    The same issue of every item re-rendering actually existing when editing or removing an item. We have now fixed all of these wasted renders. If time permits feel free to change back to the non memoized implemention of ListItem to see the wasted renders.

function ID() {
return '_' + Math.random().toString(36).substr(2, 9);
}

class Item {
constructor(id, name) {
this.id = id;
this.name = name;
}
}

const initialItems = [
new Item(ID(), 'First Item'),
new Item(ID(), 'Second Item'),
new Item(ID(), 'Third Item'),
];

function LastRendered() {
return <p>Last Rendered: {new Date().toLocaleTimeString()}</p>;
}

function Form({ item, onSubmit, onCancel, buttonValue }) {
const [inputValue, setInputValue] = React.useState(item.name || '');

const handleChange = (event) => {
setInputValue(event.target.value);
};

const handleFormSubmit = (event) => {
event.preventDefault();
const submittedItem = {
id: item ? item.id : ID(),
name: inputValue,
};

onSubmit(submittedItem);
setInputValue('');
};

const handleCancel = (event) => {
event.preventDefault();
onCancel();
};

return (
<div className="box">
<LastRendered />
<form onSubmit={handleFormSubmit}>
<input value={inputValue} onChange={handleChange} />
<button>{buttonValue || 'Save'}</button>
{onCancel && (
<a href="#" onClick={handleCancel}>
cancel
</a>
)}
</form>
</div>
);
}

//1) Wasted renders when adding item
function ListItem({ item, onEdit, onRemove }) {
return (
<div className="box">
<LastRendered />
<p>
<span>{item.name}</span>
<button onClick={() => onEdit(item)}>Edit</button>
<button onClick={() => onRemove(item)}>Remove</button>
</p>
</div>
);
}

//2) Wasted renders fixed using React.memo and custom areEqual function
// const ListItem = React.memo(
// function ListItem({ item, onEdit, onRemove }) {
// return (
// <div className="box">
// <LastRendered />
// <p>
// <span>{item.name}</span>
// <button onClick={() => onEdit(item)}>Edit</button>
// <button onClick={() => onRemove(item)}>Remove</button>
// </p>
// </div>
// );
// },
// (previous, next) => previous.item === next.item
// );

//3) Wasted renders fixed using React.memo and useCallback
// const ListItem = React.memo(function ListItem({ item, onEdit, onRemove }) {
// return (
// <div className="box">
// <LastRendered />
// <p>
// <span>{item.name}</span>
// <button onClick={() => onEdit(item)}>Edit</button>
// <button onClick={() => onRemove(item)}>Remove</button>
// </p>
// </div>
// );
// });

function List({ items, onRemove, onUpdate }) {
const [editingItem, setEditingItem] = React.useState(null);

const handleEdit = (item) => {
setEditingItem(item);
};
// const handleEdit = React.useCallback(
// (item) => {
// setEditingItem(item);
// },
// [setEditingItem]
// );

const handleCancel = () => {
setEditingItem(null);
};

return (
<div className="box">
<LastRendered />
<ul>
{items.map((item) => (
<li key={item.id}>
{item === editingItem ? (
<Form item={item} onSubmit={onUpdate} onCancel={handleCancel} />
) : (
<ListItem item={item} onEdit={handleEdit} onRemove={onRemove} />
)}
</li>
))}
</ul>
</div>
);
}

function Container() {
const [items, setItems] = React.useState([]);

React.useEffect(() => setItems(initialItems), []);

const addItem = (item) => {
setItems([...items, item]);
};

const updateItem = (updatedItem) => {
let updatedItems = items.map((item) => {
return item.id === updatedItem.id
? Object.assign({}, item, updatedItem)
: item;
});
return setItems(updatedItems);
};

const removeItem = (removeThisItem) => {
const filteredItems = items.filter((item) => item.id != removeThisItem.id);
setItems(filteredItems);
};

// const removeItem = React.useCallback((removeThisItem) => {
// const filteredItems = items.filter((item) => item.id != removeThisItem.id);
// setItems(filteredItems);
// }, setItems);

return (
<div className="box">
<LastRendered />
<Form item="" onSubmit={addItem} buttonValue="Add" />

<List items={items} onRemove={removeItem} onUpdate={updateItem} />
</div>
);
}

function App() {
return (
<div>
<Container />
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

Class Components

React.PureComponent Demo

Run the demo below and open the console to observe some wasted renders.

Steps:

  1. Paste the code below into main.js
  2. Open the application in a browser.
  3. Open Chrome DevTools and switch to the console.
  4. Type in the add textbox to add an item and then click the add button.
  5. Notice that every item in the list re-renders even though you only added one item.
  6. Commment out the ListItem component (version labeled a).
  7. Uncomment the ListItem component below the which extends React.PureComponent function (version b).
  8. Notice that the anonymous callback functions in the onClick event handlers where changed to use bind so that the same version of the function would be passed as a prop every time instead of a new instance.
    -  <button onClick={() => onEdit(item)}>Edit</button>
    - <button onClick={() => onRemove(item)}>Remove</button>
    + <button onClick={onEdit.bind(this, item)}>Edit</button>
    + <button onClick={onRemove.bind(this, item)}>Remove</button>
  9. Refresh your browser.
  10. Once again type in the add textbox to add an item and then click the add button.
  11. Notice that only one item in the list re-renders since the other ListItem's are the same. You have successfully eliminated a wasted render.
  12. Try version c) of the component which uses the shouldComponentUpdate lifecyle method to control whether the component updates and only focuses on the item prop and ignores the onEdit and onRemove callbacks.

The same issue of every item re-rendering actually existing when editing or removing an item. We have now fixed all of these wasted renders. If time permits feel free to change back to the nonpure implemention of ListItem to see the wasted renders.

function ID() {
return '_' + Math.random().toString(36).substr(2, 9);
}

class Item {
constructor(id, name) {
this.id = id;
this.name = name;
}
}

const initialItems = [
new Item(ID(), 'First Item'),
new Item(ID(), 'Second Item'),
new Item(ID(), 'Third Item'),
];

class LastRendered extends React.Component {
render() {
return <p>Last Rendered: {new Date().toLocaleTimeString()}</p>;
}
}

//a) wasted renders
class ListItem extends React.Component {
render() {
const { item, onEdit, onRemove } = this.props;
return (
<div className="box">
<LastRendered />
<p>
<span>{item.name}</span>
<button onClick={() => onEdit(item)}>Edit</button>
<button onClick={() => onRemove(item)}>Remove</button>
</p>
</div>
);
}
}

//b) pure component
// class ListItem extends React.PureComponent {
// render() {
// const { item, onEdit, onRemove } = this.props;
// return (
// <div className="box">
// <LastRendered />
// <p>
// <span>{item.name}</span>
// <button onClick={onEdit.bind(this, item)}>Edit</button>
// <button onClick={onRemove.bind(this, item)}>Remove</button>
// </p>
// </div>
// );
// }
// }

//c) shouldComponentUpdate
// class ListItem extends React.Component {
// shouldComponentUpdate(previousProps) {
// return previousProps.item !== this.props.item;
// }

// render() {
// const { item, onEdit, onRemove } = this.props;
// return (
// <div className="box">
// <LastRendered />
// <p>
// <span>{item.name}</span>
// <button onClick={() => onEdit(item)}>Edit</button>
// <button onClick={() => onRemove(item)}>Remove</button>
// </p>
// </div>
// );
// }
// }

class List extends React.Component {
state = {
editingItem: null,
};

handleEditClick = (item) => {
this.setState({ editingItem: item });
};

handleCancel = (item) => {
this.setState({ editingItem: null });
};

render() {
const { items, onRemove, onUpdate } = this.props;
return (
<div className="box">
<LastRendered />
<ul>
{items.map((item) => (
<li key={item.id}>
{item === this.state.editingItem ? (
<Form
item={item}
onSubmit={onUpdate}
onCancel={this.handleCancel}
/>
) : (
<ListItem
item={item}
onEdit={this.handleEditClick}
onRemove={onRemove}
/>
)}
</li>
))}
</ul>
</div>
);
}
}

class Form extends React.Component {
state = {
inputValue: this.props.item.name || '',
};

handleChange = (event) => {
event.preventDefault();
this.setState({ inputValue: event.target.value });
};

handleFormSubmit = (event) => {
event.preventDefault();
const item = {
id: this.props.item ? this.props.item.id : ID(),
name: this.state.inputValue,
};

this.props.onSubmit(item);
this.setState({ inputValue: '' });
};

handleCancel = (event) => {
event.preventDefault();
this.props.onCancel();
};

render() {
return (
<div className="box">
<LastRendered />
<form onSubmit={this.handleFormSubmit}>
<input value={this.state.inputValue} onChange={this.handleChange} />
<button>{this.props.buttonValue || 'Save'}</button>
{this.props.onCancel && (
<a href="#" onClick={this.handleCancel}>
cancel
</a>
)}
</form>
</div>
);
}
}

class Container extends React.Component {
state = {
items: [],
};

componentDidMount() {
this.setState({ items: initialItems });
}

addItem = (item) => {
this.setState((state) => ({ items: [...state.items, item] }));
};

updateItem = (updatedItem) => {
this.setState((state) => {
let items = state.items.map((item) => {
return item.id === updatedItem.id
? Object.assign({}, item, updatedItem)
: item;
});
return { items };
});
};

removeItem = (removeThisItem) => {
this.setState((state) => {
const items = state.items.filter((item) => item.id != removeThisItem.id);
return { items };
});
};

render() {
return (
<div className="box">
<LastRendered />
<Form item="" onSubmit={this.addItem} buttonValue="Add" />
<List
items={this.state.items}
onRemove={this.removeItem}
onUpdate={this.updateItem}
/>
</div>
);
}
}

class App extends React.Component {
render() {
return (
<div>
<Container />
</div>
);
}
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

Reference